aidevops 3.29.9 → 3.29.12
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/app.ts +16 -5
- package/packages/gui-api/src/status-managed-apps.ts +23 -8
- package/packages/gui-shared/src/notifications.ts +6 -6
- package/packages/gui-web/src/AppNavigation.tsx +5 -0
- package/packages/gui-web/src/AppWorkspace.tsx +14 -5
- package/packages/gui-web/src/InventorySurfaces.tsx +246 -72
- package/packages/gui-web/src/app-model.ts +13 -13
- package/packages/gui-web/src/styles.css +126 -31
- 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.12
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import * as readline from "node:readline";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
4
|
import { APP_ACTION_ROUTE_MANIFEST, APP_ACTION_STATUS_ROUTE_MANIFEST, BANNED_ROUTE_PATTERNS, createEnvelope, FILE_EXPLORER_ROUTE_MANIFEST, type GuiAppActionId, type GuiAppActionJobSummary, STATUS_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
|
|
4
5
|
import { readFileExplorer } from "./file-adapter";
|
|
@@ -10,6 +11,7 @@ const appActionCommands: Record<string, Partial<Record<GuiAppActionId, string[]>
|
|
|
10
11
|
"gui-desktop": { install: ["aidevops", "setup", "--scope", "gui-desktop"], update: ["aidevops", "setup", "--scope", "gui-desktop"], reinstall: ["aidevops", "setup", "--scope", "gui-desktop"] },
|
|
11
12
|
hooks: { install: ["aidevops", "setup", "--scope", "hooks"], update: ["aidevops", "setup", "--scope", "hooks"], reinstall: ["aidevops", "setup", "--scope", "hooks"] },
|
|
12
13
|
opencode: { install: ["aidevops", "setup", "--scope", "opencode"], update: ["aidevops", "setup", "--scope", "opencode"], reinstall: ["aidevops", "setup", "--scope", "opencode"] },
|
|
14
|
+
"opencode-cli": { install: ["aidevops", "setup", "--scope", "opencode"], update: ["aidevops", "setup", "--scope", "opencode"], reinstall: ["aidevops", "setup", "--scope", "opencode"] },
|
|
13
15
|
pulse: { install: ["aidevops", "setup", "--scope", "pulse"], update: ["aidevops", "setup", "--scope", "pulse"], reinstall: ["aidevops", "setup", "--scope", "pulse"] },
|
|
14
16
|
tabby: { install: ["aidevops", "setup", "--scope", "tabby"], update: ["aidevops", "setup", "--scope", "tabby"], reinstall: ["aidevops", "setup", "--scope", "tabby"] },
|
|
15
17
|
bun: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
|
|
@@ -149,10 +151,16 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
|
|
|
149
151
|
env: { ...process.env, AIDEVOPS_NON_INTERACTIVE: "true" },
|
|
150
152
|
stdio: ["ignore", "pipe", "pipe"],
|
|
151
153
|
});
|
|
152
|
-
child.stdout
|
|
153
|
-
|
|
154
|
+
if (child.stdout !== null) {
|
|
155
|
+
const stdoutLines = readline.createInterface({ input: child.stdout, terminal: false });
|
|
156
|
+
stdoutLines.on("line", (line) => appendJobLine(job, line));
|
|
157
|
+
}
|
|
158
|
+
if (child.stderr !== null) {
|
|
159
|
+
const stderrLines = readline.createInterface({ input: child.stderr, terminal: false });
|
|
160
|
+
stderrLines.on("line", (line) => appendJobLine(job, line));
|
|
161
|
+
}
|
|
154
162
|
child.on("error", (error) => {
|
|
155
|
-
|
|
163
|
+
appendJobLine(job, error.message);
|
|
156
164
|
job.status = "failed";
|
|
157
165
|
job.finished_at = new Date().toISOString();
|
|
158
166
|
job.exit_code = 127;
|
|
@@ -166,8 +174,11 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
|
|
|
166
174
|
return job;
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
function
|
|
170
|
-
|
|
177
|
+
function appendJobLine(job: GuiAppActionJobSummary, line: string): void {
|
|
178
|
+
if (line.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
job.output.push(redactOutputLine(line));
|
|
171
182
|
if (job.output.length > 400) {
|
|
172
183
|
job.output.splice(1, job.output.length - 400);
|
|
173
184
|
}
|
|
@@ -83,12 +83,12 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
|
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
id: "opencode",
|
|
86
|
-
name: "OpenCode",
|
|
87
|
-
description: "
|
|
88
|
-
category: "
|
|
89
|
-
binary:
|
|
90
|
-
version_args: [
|
|
91
|
-
install_path_refs: ["~/Applications/OpenCode AIDevOps.app", "/Applications/OpenCode.app", "~/.config/opencode/opencode.json"],
|
|
86
|
+
name: "OpenCode Beta",
|
|
87
|
+
description: "Native OpenCode beta desktop app configured by aidevops.",
|
|
88
|
+
category: "desktop",
|
|
89
|
+
binary: null,
|
|
90
|
+
version_args: [],
|
|
91
|
+
install_path_refs: ["~/Applications/OpenCode AIDevOps.app", "~/Applications/OpenCode.app", "/Applications/OpenCode.app", "~/.config/opencode/opencode.json"],
|
|
92
92
|
origin_website_url: "https://opencode.ai",
|
|
93
93
|
origin_repo_url: "",
|
|
94
94
|
aidevops_install: true,
|
|
@@ -96,6 +96,7 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
|
|
|
96
96
|
latest_version_source: "binary",
|
|
97
97
|
action_commands: setupScopeActions("opencode"),
|
|
98
98
|
},
|
|
99
|
+
binaryApp({ id: "opencode-cli", name: "OpenCode CLI", description: "Command-line runtime installed with OpenCode and configured by aidevops.", category: "cli", binary: "opencode", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/opencode", "/usr/local/bin/opencode", "~/.bun/bin/opencode", "~/.config/opencode/opencode.json"], origin_website_url: "https://opencode.ai", origin_repo_url: "", aidevops_install: true, aidevops_update: true }),
|
|
99
100
|
{
|
|
100
101
|
id: "hooks",
|
|
101
102
|
name: "Safety hooks",
|
|
@@ -139,7 +140,7 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
|
|
|
139
140
|
binaryApp({ id: "homebrew", name: "Homebrew", description: "macOS package manager used by setup/update when available.", category: "package manager", binary: "brew", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"], origin_website_url: "https://brew.sh", origin_repo_url: "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" }),
|
|
140
141
|
binaryApp({ id: "qlty", name: "Qlty CLI", description: "Optional code quality aggregator installed by setup when selected.", category: "quality", binary: "qlty", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/qlty", "/usr/local/bin/qlty", "~/.qlty/bin/qlty"], origin_website_url: "https://qlty.sh", origin_repo_url: "" }),
|
|
141
142
|
binaryApp({ id: "rtk", name: "rtk", description: "Token-optimized GitHub/API helper used by interactive discovery.", category: "developer tooling", binary: "rtk", version_args: ["--version"], install_path_refs: ["~/.local/bin/rtk", "/opt/homebrew/bin/rtk", "/usr/local/bin/rtk"], origin_website_url: "https://github.com/rtk-ai/rtk#installation", origin_repo_url: "https://github.com/rtk-ai/rtk" }),
|
|
142
|
-
binaryApp({ id: "cursor", name: "Cursor CLI", description: "Cursor command-line integration and config targets.", category: "
|
|
143
|
+
binaryApp({ id: "cursor", name: "Cursor CLI", description: "Cursor command-line integration and config targets.", category: "cli", binary: "cursor", version_args: ["--version"], install_path_refs: ["~/.cursor/bin/cursor", "/Applications/Cursor.app"], origin_website_url: "https://cursor.com/install", origin_repo_url: "" }),
|
|
143
144
|
binaryApp({ id: "zed", name: "Zed", description: "Optional editor installed by setup when selected.", category: "editor", binary: "zed", version_args: ["--version"], install_path_refs: ["/Applications/Zed.app", "/opt/homebrew/bin/zed", "/usr/local/bin/zed"], origin_website_url: "https://zed.dev/download", origin_repo_url: "", aidevops_install: false, aidevops_update: false }),
|
|
144
145
|
binaryApp({ id: "orbstack", name: "OrbStack", description: "Optional local container/VM runtime for macOS development.", category: "runtime", binary: "orb", version_args: ["version"], install_path_refs: ["/Applications/OrbStack.app", "/opt/homebrew/bin/orb"], origin_website_url: "https://orbstack.dev/", origin_repo_url: "", aidevops_install: false, aidevops_update: false }),
|
|
145
146
|
binaryApp({ id: "ollama", name: "Ollama", description: "Optional local model runtime for local AI workflows.", category: "ai runtime", binary: "ollama", version_args: ["--version"], install_path_refs: ["/Applications/Ollama.app", "/opt/homebrew/bin/ollama", "/usr/local/bin/ollama"], origin_website_url: "https://ollama.com", origin_repo_url: "" }),
|
|
@@ -221,8 +222,22 @@ function readManagedAppVersion(definition: ManagedAppDefinition, binaryPath: str
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
if (binaryPath === null) {
|
|
224
|
-
|
|
225
|
+
const existingPathRef = firstExistingPathRef(definition.install_path_refs);
|
|
226
|
+
|
|
227
|
+
if (existingPathRef === null) {
|
|
228
|
+
return "not installed";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return readAppBundleVersion(existingPathRef) ?? "installed";
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
return readBinaryVersion(binaryPath, definition.version_args);
|
|
228
235
|
}
|
|
236
|
+
|
|
237
|
+
function readAppBundleVersion(pathRef: string): string | null {
|
|
238
|
+
if (!pathRef.endsWith(".app")) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return readBinaryVersion("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleShortVersionString", `${expandHome(pathRef)}/Contents/Info.plist`]);
|
|
243
|
+
}
|
|
@@ -17,7 +17,7 @@ const toastSeverityRank: Record<GuiNotificationSeverity, number> = {
|
|
|
17
17
|
|
|
18
18
|
const warningLineMatchers: Array<(line: string) => boolean> = [
|
|
19
19
|
(line) => line.startsWith("Pulse stalled"),
|
|
20
|
-
(line) => /contribution\(s\)
|
|
20
|
+
(line) => /contribution(\(s\)|s)?\s+needs?/i.test(line),
|
|
21
21
|
(line) => line.startsWith("[OPENCODE MAINTENANCE]"),
|
|
22
22
|
(line) => line.startsWith("[WARNING]"),
|
|
23
23
|
(line) => line.startsWith("[WARN]"),
|
|
@@ -28,7 +28,7 @@ const toastTitleRules: Array<{ matches: (line: string) => boolean; title: string
|
|
|
28
28
|
{ matches: (line) => line.startsWith("[ERROR]"), title: "Startup error" },
|
|
29
29
|
{ matches: (line) => line.startsWith("[OPENCODE MAINTENANCE]"), title: "OpenCode maintenance" },
|
|
30
30
|
{ matches: (line) => line.startsWith("Pulse stalled"), title: "Pulse stalled" },
|
|
31
|
-
{ matches: (line) => /contribution\(s\)
|
|
31
|
+
{ matches: (line) => /contribution(\(s\)|s)?\s+needs?/i.test(line), title: "External contribution review" },
|
|
32
32
|
{ matches: (line) => line.startsWith("[WARNING]") || line.startsWith("[WARN]"), title: "Startup warning" },
|
|
33
33
|
{ matches: (line) => line.startsWith("Security: all protections active"), title: "Security protections active" },
|
|
34
34
|
];
|
|
@@ -93,10 +93,10 @@ function buildToastNotifications(greetingOutput: string): GuiNotificationSummary
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
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
|
|
99
|
-
const rateLimited = input.oauthPool
|
|
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
100
|
const notifications: GuiNotificationSummary[] = [];
|
|
101
101
|
|
|
102
102
|
if (input.restartRequired) {
|
|
@@ -222,6 +222,11 @@ function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, selecte
|
|
|
222
222
|
? []
|
|
223
223
|
: status.opencode_sessions.sessions.filter((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
224
224
|
const activeSessionId = sessions.some((session) => session.id_ref === selectedSessionId) ? selectedSessionId : sessions[0]?.id_ref;
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (activeSessionId !== selectedSessionId) {
|
|
227
|
+
setSelectedSessionId(activeSessionId);
|
|
228
|
+
}
|
|
229
|
+
}, [activeSessionId, selectedSessionId, setSelectedSessionId]);
|
|
225
230
|
const canSelectPreviousRepo = selectedLocalRepoIndex > 0;
|
|
226
231
|
const canSelectNextRepo = selectedLocalRepoIndex < repos.length - 1;
|
|
227
232
|
|
|
@@ -189,21 +189,30 @@ function NotificationsMenu({ notifications, openSurface }: { notifications: GuiN
|
|
|
189
189
|
<span>{active.length} active</span>
|
|
190
190
|
</div>
|
|
191
191
|
{preview.length === 0 ? <p>No current aidevops notifications.</p> : null}
|
|
192
|
-
{preview.map((notification) =>
|
|
192
|
+
{preview.map((notification) => (
|
|
193
|
+
<NotificationPreview
|
|
194
|
+
key={notification.id}
|
|
195
|
+
notification={notification}
|
|
196
|
+
onClick={() => {
|
|
197
|
+
const action = notification.actions.find((candidate) => candidate.enabled && candidate.kind === "surface" && isSurfaceId(candidate.surface_id));
|
|
198
|
+
openSurface(action?.kind === "surface" && isSurfaceId(action.surface_id) ? action.surface_id : "notifications");
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
))}
|
|
193
202
|
<button onClick={() => openSurface("notifications")} role="menuitem" type="button">Open notifications</button>
|
|
194
203
|
</div>
|
|
195
204
|
);
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
function NotificationPreview({ notification }: { notification: GuiNotificationSummary }): ReactElement {
|
|
207
|
+
function NotificationPreview({ notification, onClick }: { notification: GuiNotificationSummary; onClick: () => void }): ReactElement {
|
|
199
208
|
return (
|
|
200
|
-
<
|
|
209
|
+
<button className={`notification-preview ${notification.severity}`} onClick={onClick} role="menuitem" type="button">
|
|
201
210
|
<span aria-hidden="true"><NotificationIcon notification={notification} /></span>
|
|
202
211
|
<div>
|
|
203
212
|
<strong>{notification.title}</strong>
|
|
204
213
|
<small>{notification.category} · {notification.status}</small>
|
|
205
214
|
</div>
|
|
206
|
-
</
|
|
215
|
+
</button>
|
|
207
216
|
);
|
|
208
217
|
}
|
|
209
218
|
|
|
@@ -528,7 +537,7 @@ function NotificationCard({ notification, openSurface }: { notification: GuiNoti
|
|
|
528
537
|
return <button className="secondary-action" disabled={!action.enabled} key={action.id} onClick={() => openSurface(surfaceId)} type="button">{action.label}</button>;
|
|
529
538
|
}
|
|
530
539
|
|
|
531
|
-
return <button className="secondary-action" disabled key={action.id} title={action.command_preview} type="button">{action.label}</button>;
|
|
540
|
+
return <button className="secondary-action" disabled={!action.enabled} key={action.id} title={action.command_preview} type="button">{action.label}</button>;
|
|
532
541
|
})}
|
|
533
542
|
</div>
|
|
534
543
|
</div>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
|
-
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { type Dispatch, type MouseEvent as ReactMouseEvent, type ReactElement, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
3
|
+
import type { IconType } from "react-icons";
|
|
4
|
+
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
|
|
5
|
+
import { FiChevronDown, FiCode, FiDownload, FiExternalLink, FiGlobe, FiMonitor, FiRefreshCw, FiRepeat, FiTerminal, FiTrash2 } from "react-icons/fi";
|
|
6
|
+
import { IoLogoAndroid } from "react-icons/io";
|
|
7
|
+
import { SiIos } from "react-icons/si";
|
|
4
8
|
import type { InventoryColumn } from "./app-model";
|
|
5
9
|
import { installationRows, text } from "./app-model";
|
|
6
10
|
|
|
@@ -14,13 +18,17 @@ let draftRowCounter = 0;
|
|
|
14
18
|
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
15
19
|
const [appCollection, setAppCollection] = useState<AppCollectionId>("aidevops");
|
|
16
20
|
const [managedCategory, setManagedCategory] = useState<ManagedCategoryId>("core");
|
|
21
|
+
const [recommendedPlatform, setRecommendedPlatform] = useState<RecommendedPlatformFilterId>("all");
|
|
17
22
|
const [recommendedOs, setRecommendedOs] = useState<RecommendedOsId>("all");
|
|
18
23
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
19
24
|
const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
|
|
20
25
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
26
|
+
const [policyJobs, setPolicyJobs] = useState<Record<string, GuiAppActionJobSummary[]>>({});
|
|
27
|
+
const [policyToggles, setPolicyToggles] = useState<ManagedPolicyToggleState>(() => readManagedPolicyToggles());
|
|
21
28
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
22
|
-
const
|
|
23
|
-
const
|
|
29
|
+
const managedApps = sortedManagedApps(status.managed_apps).map((app) => applyManagedPolicyToggles(app, policyToggles[app.id]));
|
|
30
|
+
const visibleManagedApps = managedApps.filter((app) => managedCategoryForApp(app) === managedCategory);
|
|
31
|
+
const visibleRecommendedApps = recommendedApps.filter((app) => recommendedAppMatchesFilters(app, recommendedPlatform, recommendedOs));
|
|
24
32
|
|
|
25
33
|
useEffect(() => {
|
|
26
34
|
setExpandedAppId(visibleManagedApps[0]?.id ?? null);
|
|
@@ -55,22 +63,36 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
55
63
|
expanded={expandedAppId === app.id}
|
|
56
64
|
job={jobForApp(app.id, jobs, selectedJob)}
|
|
57
65
|
key={app.id}
|
|
66
|
+
policyJobs={policyJobs[app.id] ?? []}
|
|
58
67
|
onJob={(job) => {
|
|
59
68
|
setJobs((current) => ({ ...current, [job.id]: job }));
|
|
60
69
|
setSelectedJobId(job.id);
|
|
61
70
|
setExpandedAppId(app.id);
|
|
62
71
|
}}
|
|
72
|
+
onPolicyToggle={(policy, value) => {
|
|
73
|
+
setPolicyToggles((current) => {
|
|
74
|
+
const next = { ...current, [app.id]: { ...current[app.id], [policy]: value } };
|
|
75
|
+
writeManagedPolicyToggles(next);
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
const policyJob = createPolicyJob(app.id, policy, value);
|
|
79
|
+
setPolicyJobs((current) => ({ ...current, [app.id]: [...(current[app.id] ?? []), policyJob] }));
|
|
80
|
+
setSelectedJobId(policyJob.id);
|
|
81
|
+
setExpandedAppId(app.id);
|
|
82
|
+
}}
|
|
63
83
|
onToggle={() => setExpandedAppId((current) => current === app.id ? null : app.id)}
|
|
64
84
|
/>
|
|
65
85
|
))}
|
|
66
86
|
</div>
|
|
67
87
|
<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>
|
|
68
|
-
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} setOs={setRecommendedOs} />}
|
|
88
|
+
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} platform={recommendedPlatform} setOs={setRecommendedOs} setPlatform={setRecommendedPlatform} />}
|
|
69
89
|
</section>
|
|
70
90
|
);
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiManagedAppSummary; expanded: boolean; job: GuiAppActionJobSummary | null; onJob: (job: GuiAppActionJobSummary) => void; onToggle: () => void }): ReactElement {
|
|
93
|
+
function ManagedAppPanel({ app, expanded, job, onJob, onPolicyToggle, onToggle, policyJobs }: { app: GuiManagedAppSummary; expanded: boolean; job: GuiAppActionJobSummary | null; onJob: (job: GuiAppActionJobSummary) => void; onPolicyToggle: (policy: ManagedPolicyId, value: boolean) => void; onToggle: () => void; policyJobs: GuiAppActionJobSummary[] }): ReactElement {
|
|
94
|
+
const lockedPolicy = isEssentialManagedApp(app);
|
|
95
|
+
|
|
74
96
|
return (
|
|
75
97
|
<article className={expanded ? "managed-app-card expanded" : "managed-app-card"}>
|
|
76
98
|
<button aria-expanded={expanded} className="managed-app-summary" data-tooltip={`${expanded ? "Collapse" : "Expand"} ${app.name} controls`} onClick={onToggle} type="button">
|
|
@@ -94,16 +116,18 @@ function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiMana
|
|
|
94
116
|
<div className="managed-app-actions">
|
|
95
117
|
{app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} commandPreview={action.command_preview} />)}
|
|
96
118
|
</div>
|
|
97
|
-
|
|
119
|
+
</div>
|
|
98
120
|
<div className="managed-app-details">
|
|
99
|
-
<ToggleSwitch checked={app.aidevops_install} label="setup installs" />
|
|
100
|
-
<ToggleSwitch checked={app.aidevops_update} label="update maintains" />
|
|
121
|
+
<ToggleSwitch checked={app.aidevops_install} disabled={lockedPolicy} label="setup installs" onChange={(value) => onPolicyToggle("setup", value)} />
|
|
122
|
+
<ToggleSwitch checked={app.aidevops_update} disabled={lockedPolicy} label="update maintains" onChange={(value) => onPolicyToggle("update", value)} />
|
|
101
123
|
<AppMeta label="Installed" value={app.installed_version} />
|
|
102
124
|
<AppMeta label="Latest" value={app.latest_version} />
|
|
103
125
|
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
104
126
|
{job ? <AppLogLink job={job} /> : null}
|
|
105
127
|
</div>
|
|
106
|
-
{job ? <AppActionTerminal job={job} /> :
|
|
128
|
+
{job ? <AppActionTerminal job={job} /> : null}
|
|
129
|
+
{policyJobs.map((policyJob) => <AppActionTerminal job={policyJob} key={policyJob.id} />)}
|
|
130
|
+
{job === null && policyJobs.length === 0 ? <p className="empty-state compact-notice">No recent command output for this app. Run an action or change a policy toggle to open this app's terminal log.</p> : null}
|
|
107
131
|
</div> : null}
|
|
108
132
|
</article>
|
|
109
133
|
);
|
|
@@ -118,13 +142,11 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
|
|
|
118
142
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
119
143
|
}
|
|
120
144
|
|
|
121
|
-
return <a
|
|
145
|
+
return <a href={href} onClick={(event) => openExternalLink(event, href)} rel="noreferrer" target="_blank" title={href}>{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
122
146
|
}
|
|
123
147
|
|
|
124
|
-
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return <button aria-pressed={isChecked} className={isChecked ? "managed-toggle checked" : "managed-toggle"} onClick={() => setIsChecked((current) => !current)} type="button"><span aria-hidden="true" className={isChecked ? "switch-track checked" : "switch-track"}><span /></span>{label}</button>;
|
|
148
|
+
function ToggleSwitch({ checked, disabled = false, label, onChange }: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }): ReactElement {
|
|
149
|
+
return <button aria-pressed={checked} className={checked ? "managed-toggle checked" : "managed-toggle"} data-tooltip={disabled ? "Essential aidevops component; policy is locked on" : undefined} disabled={disabled} onClick={() => onChange(!checked)} title={disabled ? "Essential aidevops component; policy is locked on" : undefined} type="button"><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span>{label}</button>;
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
|
|
@@ -203,7 +225,14 @@ function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>,
|
|
|
203
225
|
return selectedJob;
|
|
204
226
|
}
|
|
205
227
|
|
|
206
|
-
|
|
228
|
+
let latestJob: GuiAppActionJobSummary | null = null;
|
|
229
|
+
for (const job of Object.values(jobs)) {
|
|
230
|
+
if (job.app_id === appId && (latestJob === null || job.started_at > latestJob.started_at)) {
|
|
231
|
+
latestJob = job;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return latestJob;
|
|
207
236
|
}
|
|
208
237
|
|
|
209
238
|
function sortedManagedApps(apps: GuiManagedAppSummary[]): GuiManagedAppSummary[] {
|
|
@@ -214,6 +243,9 @@ function managedCategoryForApp(app: GuiManagedAppSummary): ManagedCategoryId {
|
|
|
214
243
|
if (["core", "safety", "automation"].includes(app.category)) {
|
|
215
244
|
return "core";
|
|
216
245
|
}
|
|
246
|
+
if (app.category.includes("cli")) {
|
|
247
|
+
return "cli";
|
|
248
|
+
}
|
|
217
249
|
if (app.category.includes("ai")) {
|
|
218
250
|
return "ai";
|
|
219
251
|
}
|
|
@@ -227,9 +259,17 @@ function managedCategoryForApp(app: GuiManagedAppSummary): ManagedCategoryId {
|
|
|
227
259
|
return "cli";
|
|
228
260
|
}
|
|
229
261
|
|
|
262
|
+
function isEssentialManagedApp(app: GuiManagedAppSummary): boolean {
|
|
263
|
+
return ["core", "safety", "automation"].includes(app.category);
|
|
264
|
+
}
|
|
265
|
+
|
|
230
266
|
type AppCollectionId = "aidevops" | "recommended";
|
|
231
267
|
type ManagedCategoryId = "core" | "ai" | "cli" | "desktop" | "terminal";
|
|
232
268
|
type RecommendedOsId = "all" | "macos" | "linux" | "windows" | "ios" | "android";
|
|
269
|
+
type RecommendedPlatformId = "webapp" | "saas" | "cli" | "api";
|
|
270
|
+
type RecommendedPlatformFilterId = "all" | RecommendedPlatformId;
|
|
271
|
+
type ManagedPolicyId = "setup" | "update";
|
|
272
|
+
type ManagedPolicyToggleState = Record<string, Partial<Record<ManagedPolicyId, boolean>>>;
|
|
233
273
|
|
|
234
274
|
interface TabOption<T extends string> {
|
|
235
275
|
id: T;
|
|
@@ -242,7 +282,12 @@ interface RecommendedApp {
|
|
|
242
282
|
websiteUrl: string;
|
|
243
283
|
alternativeToUrl?: string;
|
|
244
284
|
repoUrl?: string;
|
|
285
|
+
iosUrl?: string;
|
|
286
|
+
androidUrl?: string;
|
|
287
|
+
fdroidUrl?: string;
|
|
288
|
+
obtainiumUrl?: string;
|
|
245
289
|
os: RecommendedOsId[];
|
|
290
|
+
platforms: RecommendedPlatformId[];
|
|
246
291
|
}
|
|
247
292
|
|
|
248
293
|
const appCollectionTabs: TabOption<AppCollectionId>[] = [
|
|
@@ -258,8 +303,16 @@ const managedCategoryTabs: TabOption<ManagedCategoryId>[] = [
|
|
|
258
303
|
{ id: "terminal", label: "Terminal" },
|
|
259
304
|
];
|
|
260
305
|
|
|
261
|
-
const
|
|
306
|
+
const recommendedPlatformTabs: TabOption<RecommendedPlatformFilterId>[] = [
|
|
262
307
|
{ id: "all", label: "All" },
|
|
308
|
+
{ id: "webapp", label: "WebApp" },
|
|
309
|
+
{ id: "saas", label: "SaaS" },
|
|
310
|
+
{ id: "cli", label: "CLI" },
|
|
311
|
+
{ id: "api", label: "API" },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const recommendedOsTabs: TabOption<RecommendedOsId>[] = [
|
|
315
|
+
{ id: "all", label: "All OS" },
|
|
263
316
|
{ id: "macos", label: "macOS" },
|
|
264
317
|
{ id: "linux", label: "Linux" },
|
|
265
318
|
{ id: "windows", label: "Windows" },
|
|
@@ -268,89 +321,210 @@ const recommendedOsTabs: TabOption<RecommendedOsId>[] = [
|
|
|
268
321
|
];
|
|
269
322
|
|
|
270
323
|
const recommendedApps = ([
|
|
271
|
-
{ name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"] },
|
|
272
|
-
{ name: "Bitwarden", description: "Open source password manager.", websiteUrl: "https://bitwarden.com", alternativeToUrl: "https://alternativeto.net/software/bitwarden--free-password-manager/", repoUrl: "https://github.com/bitwarden", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
273
|
-
{ name: "Brave Browser", description: "Privacy-focused web browser.", websiteUrl: "https://brave.com/", alternativeToUrl: "https://alternativeto.net/software/brave/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
274
|
-
{ name: "Cloudron", description: "Self-hosted app platform.", websiteUrl: "https://www.cloudron.io/", alternativeToUrl: "https://alternativeto.net/software/cloudron/", os: ["linux"] },
|
|
275
|
-
{ name: "Collabora Online", description: "Online office collaboration.", websiteUrl: "https://www.collaboraonline.com/collabora-online/", alternativeToUrl: "https://alternativeto.net/software/collabora-online/", os: ["linux"] },
|
|
276
|
-
{ name: "Cometly", description: "Marketing attribution analytics.", websiteUrl: "https://www.cometly.com/", os: [] },
|
|
277
|
-
{ name: "DaVinci Resolve", description: "Professional video editing, color, VFX, and audio post-production.", websiteUrl: "https://www.blackmagicdesign.com/products/davinciresolve/", alternativeToUrl: "https://alternativeto.net/software/davinci-resolve/", os: ["macos", "linux", "windows"] },
|
|
278
|
-
{ name: "DocuSeal", description: "Open source document signing.", websiteUrl: "https://www.docuseal.com/", alternativeToUrl: "https://alternativeto.net/software/docuseal/", os: ["linux"] },
|
|
279
|
-
{ name: "Element", description: "Matrix messaging client.", websiteUrl: "https://element.io/", alternativeToUrl: "https://alternativeto.net/software/element-app/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
280
|
-
{ name: "Enpass", description: "Offline-first password manager.", websiteUrl: "https://www.enpass.io/", alternativeToUrl: "https://alternativeto.net/software/enpass/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
281
|
-
{ name: "EspoCRM", description: "Open source CRM.", websiteUrl: "https://www.espocrm.com/", alternativeToUrl: "https://alternativeto.net/software/espocrm/", os: ["linux"] },
|
|
282
|
-
{ name: "Fathom Analytics", description: "Privacy-first analytics.", websiteUrl: "https://usefathom.com/", alternativeToUrl: "https://alternativeto.net/software/fathom-analytics/", os: [] },
|
|
283
|
-
{ name: "
|
|
284
|
-
{ name: "Forgejo", description: "Self-hosted Git forge.", websiteUrl: "https://forgejo.org/", alternativeToUrl: "https://alternativeto.net/software/forgejo/", os: ["linux"] },
|
|
285
|
-
{ name: "Ghost", description: "Publishing platform.", websiteUrl: "https://ghost.org/", alternativeToUrl: "https://alternativeto.net/software/ghost/", os: ["linux"] },
|
|
286
|
-
{ name: "Gitea", description: "Self-hosted Git service.", websiteUrl: "https://about.gitea.com/", alternativeToUrl: "https://alternativeto.net/software/gitea/", os: ["linux", "windows"] },
|
|
287
|
-
{ name: "GitLab", description: "DevSecOps and Git hosting platform.", websiteUrl: "https://gitlab.com/", alternativeToUrl: "https://alternativeto.net/software/gitlab/", os: ["linux"] },
|
|
288
|
-
{ name: "LibreOffice", description: "Open source office suite.", websiteUrl: "https://www.libreoffice.org/", alternativeToUrl: "https://alternativeto.net/software/libreoffice/", os: ["macos", "linux", "windows"] },
|
|
289
|
-
{ name: "LocalWP", description: "Local WordPress development.", websiteUrl: "https://localwp.com/", alternativeToUrl: "https://alternativeto.net/software/local-by-flywheel/", os: ["macos", "linux", "windows"] },
|
|
290
|
-
{ name: "Matomo", description: "Open analytics platform.", websiteUrl: "https://matomo.org/", alternativeToUrl: "https://alternativeto.net/software/piwik/", os: ["linux"] },
|
|
291
|
-
{ name: "Nextcloud", description: "Open source content collaboration platform.", websiteUrl: "https://nextcloud.com/", alternativeToUrl: "https://alternativeto.net/software/nextcloud/", repoUrl: "https://github.com/nextcloud", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
292
|
-
{ name: "
|
|
293
|
-
{ name: "
|
|
294
|
-
{ name: "
|
|
295
|
-
{ name: "
|
|
296
|
-
{ name: "
|
|
297
|
-
{ name: "
|
|
298
|
-
{ name: "
|
|
299
|
-
{ name: "
|
|
300
|
-
{ name: "
|
|
301
|
-
{ name: "
|
|
302
|
-
{ name: "
|
|
303
|
-
{ name: "
|
|
304
|
-
{ name: "
|
|
305
|
-
{ name: "
|
|
306
|
-
{ name: "
|
|
307
|
-
{ name: "
|
|
308
|
-
{ name: "
|
|
309
|
-
{ name: "
|
|
310
|
-
{ name: "
|
|
311
|
-
{ name: "
|
|
312
|
-
{ name: "
|
|
313
|
-
{ name: "
|
|
314
|
-
{ name: "
|
|
315
|
-
{ name: "
|
|
316
|
-
{ name: "
|
|
324
|
+
{ name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"], platforms: [] },
|
|
325
|
+
{ name: "Bitwarden", description: "Open source password manager.", websiteUrl: "https://bitwarden.com", alternativeToUrl: "https://alternativeto.net/software/bitwarden--free-password-manager/", repoUrl: "https://github.com/bitwarden", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
326
|
+
{ name: "Brave Browser", description: "Privacy-focused web browser.", websiteUrl: "https://brave.com/", alternativeToUrl: "https://alternativeto.net/software/brave/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
|
|
327
|
+
{ name: "Cloudron", description: "Self-hosted app platform.", websiteUrl: "https://www.cloudron.io/", alternativeToUrl: "https://alternativeto.net/software/cloudron/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
|
|
328
|
+
{ name: "Collabora Online", description: "Online office collaboration.", websiteUrl: "https://www.collaboraonline.com/collabora-online/", alternativeToUrl: "https://alternativeto.net/software/collabora-online/", os: ["linux"], platforms: ["webapp", "api"] },
|
|
329
|
+
{ name: "Cometly", description: "Marketing attribution analytics.", websiteUrl: "https://www.cometly.com/", os: [], platforms: ["saas", "api"] },
|
|
330
|
+
{ name: "DaVinci Resolve", description: "Professional video editing, color, VFX, and audio post-production.", websiteUrl: "https://www.blackmagicdesign.com/products/davinciresolve/", alternativeToUrl: "https://alternativeto.net/software/davinci-resolve/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
331
|
+
{ name: "DocuSeal", description: "Open source document signing.", websiteUrl: "https://www.docuseal.com/", alternativeToUrl: "https://alternativeto.net/software/docuseal/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
332
|
+
{ name: "Element", description: "Matrix messaging client.", websiteUrl: "https://element.io/", alternativeToUrl: "https://alternativeto.net/software/element-app/", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp"] },
|
|
333
|
+
{ name: "Enpass", description: "Offline-first password manager.", websiteUrl: "https://www.enpass.io/", alternativeToUrl: "https://alternativeto.net/software/enpass/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
|
|
334
|
+
{ name: "EspoCRM", description: "Open source CRM.", websiteUrl: "https://www.espocrm.com/", alternativeToUrl: "https://alternativeto.net/software/espocrm/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
335
|
+
{ name: "Fathom Analytics", description: "Privacy-first analytics.", websiteUrl: "https://usefathom.com/", alternativeToUrl: "https://alternativeto.net/software/fathom-analytics/", os: [], platforms: ["saas", "api"] },
|
|
336
|
+
{ name: "FontBase", description: "Font management and reference tool.", websiteUrl: "https://fontba.se/", alternativeToUrl: "https://alternativeto.net/software/fontbase/", os: ["macos"], platforms: [] },
|
|
337
|
+
{ name: "Forgejo", description: "Self-hosted Git forge.", websiteUrl: "https://forgejo.org/", alternativeToUrl: "https://alternativeto.net/software/forgejo/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
|
|
338
|
+
{ name: "Ghost", description: "Publishing platform.", websiteUrl: "https://ghost.org/", alternativeToUrl: "https://alternativeto.net/software/ghost/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
339
|
+
{ name: "Gitea", description: "Self-hosted Git service.", websiteUrl: "https://about.gitea.com/", alternativeToUrl: "https://alternativeto.net/software/gitea/", os: ["linux", "windows"], platforms: ["webapp", "cli", "api"] },
|
|
340
|
+
{ name: "GitLab", description: "DevSecOps and Git hosting platform.", websiteUrl: "https://gitlab.com/", alternativeToUrl: "https://alternativeto.net/software/gitlab/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
341
|
+
{ name: "LibreOffice", description: "Open source office suite.", websiteUrl: "https://www.libreoffice.org/", alternativeToUrl: "https://alternativeto.net/software/libreoffice/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
342
|
+
{ name: "LocalWP", description: "Local WordPress development.", websiteUrl: "https://localwp.com/", alternativeToUrl: "https://alternativeto.net/software/local-by-flywheel/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
343
|
+
{ name: "Matomo", description: "Open analytics platform.", websiteUrl: "https://matomo.org/", alternativeToUrl: "https://alternativeto.net/software/piwik/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
344
|
+
{ name: "Nextcloud", description: "Open source content collaboration platform.", websiteUrl: "https://nextcloud.com/", alternativeToUrl: "https://alternativeto.net/software/nextcloud/", repoUrl: "https://github.com/nextcloud", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "api"] },
|
|
345
|
+
{ name: "Nextcloud Talk", description: "Open source video calls, chat, and collaboration for Nextcloud.", websiteUrl: "https://nextcloud.com/talk/", repoUrl: "https://github.com/nextcloud/spreed", iosUrl: "https://apps.apple.com/us/app/nextcloud-talk/id1296825574", androidUrl: "https://play.google.com/store/apps/details?id=com.nextcloud.talk2&hl=en", os: ["ios", "android"], platforms: ["webapp"] },
|
|
346
|
+
{ name: "OBS Studio", description: "Open source video recording and live streaming.", websiteUrl: "https://obsproject.com/", alternativeToUrl: "https://alternativeto.net/software/open-broadcaster-software/", repoUrl: "https://github.com/obsproject/obs-studio", os: ["macos", "linux", "windows"], platforms: [] },
|
|
347
|
+
{ name: "ONLYOFFICE", description: "Office and document collaboration.", websiteUrl: "https://www.onlyoffice.com/", alternativeToUrl: "https://alternativeto.net/software/onlyoffice/", repoUrl: "https://github.com/ONLYOFFICE/", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "api"] },
|
|
348
|
+
{ name: "OpenScreen", description: "Open screen-sharing project.", websiteUrl: "https://github.com/getopenscreen/openscreen/releases", alternativeToUrl: "https://alternativeto.net/software/openscreen/", repoUrl: "https://github.com/getopenscreen/openscreen", os: ["macos", "linux", "windows"], platforms: [] },
|
|
349
|
+
{ name: "Osaurus", description: "AI app workspace.", websiteUrl: "https://osaurus.ai/", alternativeToUrl: "https://alternativeto.net/software/osaurus/", os: ["macos"], platforms: [] },
|
|
350
|
+
{ name: "Parallels Desktop", description: "Run Windows, Linux, and virtual machines on macOS.", websiteUrl: "https://www.parallels.com/products/desktop/", alternativeToUrl: "https://alternativeto.net/software/parallels-desktop/about/", os: ["macos"], platforms: [] },
|
|
351
|
+
{ name: "PDF Studio", description: "PDF editor.", websiteUrl: "https://www.qoppa.com/pdfstudio/", alternativeToUrl: "https://alternativeto.net/software/qoppa-pdf-studio/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
352
|
+
{ name: "Pixelmator Pro", description: "Professional image editor for macOS.", websiteUrl: "https://www.apple.com/pixelmator-pro/", os: ["macos"], platforms: [] },
|
|
353
|
+
{ name: "PostHog", description: "Product analytics platform.", websiteUrl: "https://posthog.com/", alternativeToUrl: "https://alternativeto.net/software/posthog/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
354
|
+
{ name: "Postiz", description: "Social media scheduling.", websiteUrl: "https://postiz.com/", alternativeToUrl: "https://alternativeto.net/software/postiz/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
355
|
+
{ name: "PrivateBin", description: "Zero-knowledge pastebin.", websiteUrl: "https://privatebin.info/", alternativeToUrl: "https://alternativeto.net/software/privatebin/", repoUrl: "https://github.com/PrivateBin/PrivateBin", os: ["linux"], platforms: ["webapp", "api"] },
|
|
356
|
+
{ name: "Proxmox", description: "Virtualization platform.", websiteUrl: "https://www.proxmox.com/", alternativeToUrl: "https://alternativeto.net/software/proxmox-virtual-environment/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
|
|
357
|
+
{ name: "QuickFile", description: "Accounting platform.", websiteUrl: "https://www.quickfile.co.uk/", os: [], platforms: ["saas", "api"] },
|
|
358
|
+
{ name: "Reframed", description: "Creative framing utility.", websiteUrl: "https://www.reframed.dev/", alternativeToUrl: "https://alternativeto.net/software/reframed/", os: ["macos"], platforms: [] },
|
|
359
|
+
{ name: "Rybbit", description: "Web analytics.", websiteUrl: "https://rybbit.com/", alternativeToUrl: "https://alternativeto.net/software/rybbit/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
360
|
+
{ name: "SchildiChat", description: "Matrix messaging client.", websiteUrl: "https://schildi.chat/", alternativeToUrl: "https://alternativeto.net/software/schildichat/", os: ["macos", "linux", "windows", "android"], platforms: ["webapp"] },
|
|
361
|
+
{ name: "ScreenFlow", description: "Screen recording and editing.", websiteUrl: "https://www.telestream.net/screenflow/", alternativeToUrl: "https://alternativeto.net/software/screenflow/", os: ["macos", "ios"], platforms: [] },
|
|
362
|
+
{ name: "SEO Utils", description: "Desktop SEO tools.", websiteUrl: "https://seoutils.app/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
363
|
+
{ name: "Shottr", description: "macOS screenshot utility.", websiteUrl: "https://shottr.cc/", alternativeToUrl: "https://alternativeto.net/software/shottr/", os: ["macos"], platforms: [] },
|
|
364
|
+
{ name: "Signal", description: "Private messenger.", websiteUrl: "https://signal.org/", alternativeToUrl: "https://alternativeto.net/software/signal-private-messenger/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
|
|
365
|
+
{ name: "SimpleX Chat", description: "Private messenger with no user IDs.", websiteUrl: "https://simplex.chat/", alternativeToUrl: "https://alternativeto.net/software/simplex-chat/about/", repoUrl: "https://github.com/simplex-chat", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["cli"] },
|
|
366
|
+
{ name: "Telegram", description: "Cloud-based mobile and desktop messaging app.", websiteUrl: "https://telegram.org/", iosUrl: "https://telegram.org/dl/ios", androidUrl: "https://telegram.org/android", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "api"] },
|
|
367
|
+
{ name: "Thunderbird", description: "Email and calendar app.", websiteUrl: "https://www.thunderbird.net/", alternativeToUrl: "https://alternativeto.net/software/mozilla-thunderbird/about/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
368
|
+
{ name: "Ubicloud", description: "Open cloud platform.", websiteUrl: "https://www.ubicloud.com/", alternativeToUrl: "https://alternativeto.net/software/ubicloud/about/", repoUrl: "https://github.com/ubicloud/ubicloud", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
369
|
+
{ name: "Vaultwarden", description: "Alternative Bitwarden server implementation.", websiteUrl: "https://github.com/dani-garcia/vaultwarden", alternativeToUrl: "https://alternativeto.net/software/vaultwarden/", repoUrl: "https://github.com/dani-garcia/vaultwarden", os: ["linux"], platforms: ["webapp", "api"] },
|
|
370
|
+
{ name: "VideoProc", description: "Video processing toolkit.", websiteUrl: "https://www.videoproc.com/", alternativeToUrl: "https://alternativeto.net/software/videoproc/about/", os: ["macos", "windows"], platforms: [] },
|
|
371
|
+
{ name: "VirtualBox", description: "Virtualization app.", websiteUrl: "https://www.virtualbox.org/", alternativeToUrl: "https://alternativeto.net/software/virtualbox/about/", os: ["macos", "linux", "windows"], platforms: ["cli"] },
|
|
372
|
+
{ name: "WordPress", description: "Open source publishing platform.", websiteUrl: "https://wordpress.org/", alternativeToUrl: "https://alternativeto.net/software/wordpress/about/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
317
373
|
] satisfies RecommendedApp[]).sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
318
374
|
|
|
319
375
|
function TabNav<T extends string>({ label, onChange, tabs, value }: { label: string; onChange: (value: T) => void; tabs: TabOption<T>[]; value: T }): ReactElement {
|
|
320
376
|
return <div aria-label={label} className="pill-tabs app-subnav" role="tablist">{tabs.map((tab) => <button aria-selected={value === tab.id} className={value === tab.id ? "active" : ""} key={tab.id} onClick={() => onChange(tab.id)} role="tab" type="button">{tab.label}</button>)}</div>;
|
|
321
377
|
}
|
|
322
378
|
|
|
323
|
-
function RecommendedAppsSurface({ apps, os, setOs }: { apps: RecommendedApp[]; os: RecommendedOsId; setOs: (os: RecommendedOsId) => void }): ReactElement {
|
|
379
|
+
function RecommendedAppsSurface({ apps, os, platform, setOs, setPlatform }: { apps: RecommendedApp[]; os: RecommendedOsId; platform: RecommendedPlatformFilterId; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
324
380
|
return <>
|
|
325
|
-
<
|
|
381
|
+
<div className="recommended-filter-tabs">
|
|
382
|
+
<TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
|
|
383
|
+
<TabNav label="Recommended app operating system filters" tabs={recommendedOsTabs} value={os} onChange={setOs} />
|
|
384
|
+
</div>
|
|
326
385
|
<div className="recommended-app-grid">
|
|
327
|
-
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} />)}
|
|
386
|
+
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} setOs={setOs} setPlatform={setPlatform} />)}
|
|
328
387
|
</div>
|
|
329
388
|
</>;
|
|
330
389
|
}
|
|
331
390
|
|
|
332
|
-
function RecommendedAppCard({ app }: { app: RecommendedApp }): ReactElement {
|
|
391
|
+
function RecommendedAppCard({ app, setOs, setPlatform }: { app: RecommendedApp; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
333
392
|
return <article className="recommended-app-card">
|
|
334
393
|
<div>
|
|
335
394
|
<strong>{app.name}</strong>
|
|
336
395
|
<p>{app.description}</p>
|
|
337
396
|
</div>
|
|
338
|
-
<
|
|
397
|
+
<div className="app-icon-groups">
|
|
398
|
+
<PlatformIconList platforms={app.platforms} setPlatform={setPlatform} />
|
|
399
|
+
<OsIconList os={app.os} setOs={setOs} />
|
|
400
|
+
</div>
|
|
339
401
|
<div className="managed-app-links plain-links">
|
|
340
402
|
<OriginLink href={app.websiteUrl} label={text.website} />
|
|
341
403
|
{app.alternativeToUrl ? <OriginLink href={app.alternativeToUrl} label="AlternativeTo" /> : null}
|
|
342
404
|
{app.repoUrl ? <OriginLink href={app.repoUrl} label="Repo" /> : null}
|
|
405
|
+
{app.iosUrl ? <OriginLink href={app.iosUrl} label="iOS" /> : null}
|
|
406
|
+
{app.androidUrl ? <OriginLink href={app.androidUrl} label="Android" /> : null}
|
|
407
|
+
{app.fdroidUrl ? <OriginLink href={app.fdroidUrl} label="F-Droid" /> : null}
|
|
408
|
+
{app.obtainiumUrl ? <OriginLink href={app.obtainiumUrl} label="Obtainium" /> : null}
|
|
343
409
|
</div>
|
|
344
410
|
</article>;
|
|
345
411
|
}
|
|
346
412
|
|
|
347
|
-
function
|
|
348
|
-
|
|
413
|
+
function PlatformIconList({ platforms, setPlatform }: { platforms: RecommendedPlatformId[]; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement | null {
|
|
414
|
+
if (platforms.length === 0) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return <span className="os-icon-list platform-icon-list">{platforms.map((platform) => <PlatformIcon id={platform} key={platform} setPlatform={setPlatform} />)}</span>;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function PlatformIcon({ id, setPlatform }: { id: RecommendedPlatformId; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
422
|
+
const iconMap: Record<RecommendedPlatformId, { Icon: IconType; label: string }> = {
|
|
423
|
+
webapp: { Icon: FiMonitor, label: "WebApp" },
|
|
424
|
+
saas: { Icon: FiGlobe, label: "SaaS" },
|
|
425
|
+
cli: { Icon: FiTerminal, label: "CLI" },
|
|
426
|
+
api: { Icon: FiCode, label: "API" },
|
|
427
|
+
};
|
|
428
|
+
const { Icon, label } = iconMap[id];
|
|
429
|
+
|
|
430
|
+
return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setPlatform(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function OsIconList({ os, setOs }: { os: RecommendedOsId[]; setOs: (os: RecommendedOsId) => void }): ReactElement | null {
|
|
349
434
|
if (os.length === 0) {
|
|
350
|
-
return
|
|
435
|
+
return null;
|
|
351
436
|
}
|
|
352
437
|
|
|
353
|
-
return <span className="os-icon-list">{os.map((item) => <
|
|
438
|
+
return <span className="os-icon-list">{os.map((item) => <OsIcon id={item} key={item} setOs={setOs} />)}</span>;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function OsIcon({ id, setOs }: { id: RecommendedOsId; setOs: (os: RecommendedOsId) => void }): ReactElement {
|
|
442
|
+
const iconMap: Record<RecommendedOsId, { Icon: IconType; label: string }> = {
|
|
443
|
+
all: { Icon: FiGlobe, label: "Web app" },
|
|
444
|
+
macos: { Icon: FaApple, label: "macOS" },
|
|
445
|
+
linux: { Icon: FaLinux, label: "Linux" },
|
|
446
|
+
windows: { Icon: FaWindows, label: "Windows" },
|
|
447
|
+
ios: { Icon: SiIos, label: "iOS" },
|
|
448
|
+
android: { Icon: IoLogoAndroid, label: "Android" },
|
|
449
|
+
};
|
|
450
|
+
const { Icon, label } = iconMap[id];
|
|
451
|
+
|
|
452
|
+
return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setOs(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function recommendedAppMatchesFilters(app: RecommendedApp, platform: RecommendedPlatformFilterId, os: RecommendedOsId): boolean {
|
|
456
|
+
const platformMatches = platform === "all" || app.platforms.includes(platform);
|
|
457
|
+
const osMatches = os === "all" || app.os.includes(os);
|
|
458
|
+
|
|
459
|
+
return platformMatches && osMatches;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function applyManagedPolicyToggles(app: GuiManagedAppSummary, policy: Partial<Record<ManagedPolicyId, boolean>> | undefined): GuiManagedAppSummary {
|
|
463
|
+
if (policy === undefined) {
|
|
464
|
+
return app;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
...app,
|
|
469
|
+
aidevops_install: policy.setup ?? app.aidevops_install,
|
|
470
|
+
aidevops_update: policy.update ?? app.aidevops_update,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function createPolicyJob(appId: string, policy: ManagedPolicyId, value: boolean): GuiAppActionJobSummary {
|
|
475
|
+
const mode = value ? "on" : "off";
|
|
476
|
+
const command = `aidevops app policy set ${appId} --${policy === "setup" ? "setup-installs" : "update-maintains"} ${mode}`;
|
|
477
|
+
const now = new Date().toISOString();
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
id: `policy-${appId}-${policy}-${Date.now()}`,
|
|
481
|
+
app_id: appId,
|
|
482
|
+
action: "update",
|
|
483
|
+
status: "completed",
|
|
484
|
+
command_preview: command,
|
|
485
|
+
started_at: now,
|
|
486
|
+
finished_at: now,
|
|
487
|
+
exit_code: 0,
|
|
488
|
+
output: [
|
|
489
|
+
`$ ${command}`,
|
|
490
|
+
`Saved local aidevops.app policy override for ${appId}: ${policy}=${mode}.`,
|
|
491
|
+
"Persistence: browser localStorage key aidevops-gui-managed-app-policy.",
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function readManagedPolicyToggles(): ManagedPolicyToggleState {
|
|
497
|
+
if (typeof window === "undefined") {
|
|
498
|
+
return {};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const raw = window.localStorage.getItem("aidevops-gui-managed-app-policy");
|
|
503
|
+
return raw === null ? {} : JSON.parse(raw) as ManagedPolicyToggleState;
|
|
504
|
+
} catch {
|
|
505
|
+
return {};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function writeManagedPolicyToggles(policy: ManagedPolicyToggleState): void {
|
|
510
|
+
if (typeof window === "undefined") {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
window.localStorage.setItem("aidevops-gui-managed-app-policy", JSON.stringify(policy));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function openExternalLink(event: ReactMouseEvent<HTMLAnchorElement>, href: string): void {
|
|
518
|
+
event.stopPropagation();
|
|
519
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const opened = window.open(href, "_blank", "noopener,noreferrer");
|
|
524
|
+
if (opened !== null) {
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
opened.opener = null;
|
|
527
|
+
}
|
|
354
528
|
}
|
|
355
529
|
|
|
356
530
|
export function InstallationSurface(): ReactElement {
|
|
@@ -335,6 +335,19 @@ function plannedCommsGroup(label: string, items: SurfaceNavItem[]): SurfaceNavGr
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
export const navGroups: SurfaceNavGroup[] = [
|
|
338
|
+
{
|
|
339
|
+
label: text.infrastructure,
|
|
340
|
+
mode: "devops",
|
|
341
|
+
items: [
|
|
342
|
+
{ id: "devices", label: text.devices, description: "Installed aidevops machines", icon: "device", badge: text.planned },
|
|
343
|
+
{ id: "apps", label: text.apps, description: "Installed tools and apps", icon: "package" },
|
|
344
|
+
{ id: "vpnsProxies", label: text.vpnsProxies, description: "VPN and proxy inventory", icon: "shield", badge: text.planned },
|
|
345
|
+
{ id: "installation", label: text.installation, description: "Optional setup toggles", icon: "download" },
|
|
346
|
+
{ id: "registrars", label: text.registrars, description: "Domain registrars", icon: "chain" },
|
|
347
|
+
{ id: "hosts", label: text.hosts, description: "Hosting providers", icon: "server" },
|
|
348
|
+
{ id: "servers", label: text.servers, description: "Servers and orchestrators", icon: "hardDrive" },
|
|
349
|
+
],
|
|
350
|
+
},
|
|
338
351
|
{
|
|
339
352
|
label: text.development,
|
|
340
353
|
mode: "devops",
|
|
@@ -360,19 +373,6 @@ export const navGroups: SurfaceNavGroup[] = [
|
|
|
360
373
|
{ id: "routines", label: text.routines, description: "Scheduled workflows", icon: "clock", badge: text.planned },
|
|
361
374
|
],
|
|
362
375
|
},
|
|
363
|
-
{
|
|
364
|
-
label: text.infrastructure,
|
|
365
|
-
mode: "devops",
|
|
366
|
-
items: [
|
|
367
|
-
{ id: "devices", label: text.devices, description: "Installed aidevops machines", icon: "device", badge: text.planned },
|
|
368
|
-
{ id: "vpnsProxies", label: text.vpnsProxies, description: "VPN and proxy inventory", icon: "shield", badge: text.planned },
|
|
369
|
-
{ id: "apps", label: text.apps, description: "Installed tools and apps", icon: "package" },
|
|
370
|
-
{ id: "installation", label: text.installation, description: "Optional setup toggles", icon: "download" },
|
|
371
|
-
{ id: "registrars", label: text.registrars, description: "Domain registrars", icon: "chain" },
|
|
372
|
-
{ id: "hosts", label: text.hosts, description: "Hosting providers", icon: "server" },
|
|
373
|
-
{ id: "servers", label: text.servers, description: "Servers and orchestrators", icon: "hardDrive" },
|
|
374
|
-
],
|
|
375
|
-
},
|
|
376
376
|
{
|
|
377
377
|
label: text.identities,
|
|
378
378
|
mode: "comms",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--accent-hue: 123;
|
|
3
3
|
--accent: hsl(var(--accent-hue) 50% 36%);
|
|
4
|
+
--accent-aaa-bg: hsl(var(--accent-hue) 58% 24%);
|
|
4
5
|
--accent-strong: hsl(var(--accent-hue) 56% 28%);
|
|
5
6
|
--bg-primary: #ffffff;
|
|
6
7
|
--bg-secondary: #f8f8f8;
|
|
@@ -12,6 +13,14 @@
|
|
|
12
13
|
--font-family-app: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
13
14
|
--font-size-app: 16px;
|
|
14
15
|
--glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 58%), inset 0 -1px 0 rgb(0 0 0 / 3%), var(--shadow-soft);
|
|
16
|
+
--notification-error-bg: rgb(207 34 46 / 10%);
|
|
17
|
+
--notification-error-fg: #cf222e;
|
|
18
|
+
--notification-info-bg: rgb(9 105 218 / 10%);
|
|
19
|
+
--notification-info-fg: #0969da;
|
|
20
|
+
--notification-success-bg: rgb(26 127 55 / 10%);
|
|
21
|
+
--notification-success-fg: #1a7f37;
|
|
22
|
+
--notification-warning-bg: rgb(191 135 0 / 12%);
|
|
23
|
+
--notification-warning-fg: #9a6700;
|
|
15
24
|
--shell-gutter: 8px;
|
|
16
25
|
--shadow-soft: 0 18px 56px rgb(15 23 42 / 9%);
|
|
17
26
|
--surface-elevated: #ffffff;
|
|
@@ -33,6 +42,7 @@
|
|
|
33
42
|
|
|
34
43
|
:root[data-theme="dark"] {
|
|
35
44
|
--accent: hsl(var(--accent-hue) 74% 66%);
|
|
45
|
+
--accent-aaa-bg: hsl(var(--accent-hue) 64% 24%);
|
|
36
46
|
--accent-strong: hsl(var(--accent-hue) 75% 72%);
|
|
37
47
|
--bg-primary: #0a0a0a;
|
|
38
48
|
--bg-secondary: #111111;
|
|
@@ -41,6 +51,14 @@
|
|
|
41
51
|
--border-hover: color-mix(in srgb, rgb(255 255 255 / 22%) 74%, rgb(255 255 255 / 40%));
|
|
42
52
|
--code-bg: hsl(var(--accent-hue) 18% 6%);
|
|
43
53
|
--glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 10%), inset 0 -1px 0 rgb(0 0 0 / 24%), var(--shadow-soft);
|
|
54
|
+
--notification-error-bg: rgb(255 123 114 / 13%);
|
|
55
|
+
--notification-error-fg: #ff7b72;
|
|
56
|
+
--notification-info-bg: rgb(88 166 255 / 13%);
|
|
57
|
+
--notification-info-fg: #58a6ff;
|
|
58
|
+
--notification-success-bg: rgb(86 211 100 / 13%);
|
|
59
|
+
--notification-success-fg: #56d364;
|
|
60
|
+
--notification-warning-bg: rgb(210 153 34 / 15%);
|
|
61
|
+
--notification-warning-fg: #d29922;
|
|
44
62
|
--shadow-soft: 0 24px 80px rgb(0 0 0 / 40%);
|
|
45
63
|
--surface-elevated: #151515;
|
|
46
64
|
--surface-muted: #202020;
|
|
@@ -921,6 +939,15 @@ code {
|
|
|
921
939
|
min-width: 0;
|
|
922
940
|
}
|
|
923
941
|
|
|
942
|
+
.header-actions > .header-icon-button,
|
|
943
|
+
.header-actions > .header-action-menu {
|
|
944
|
+
align-items: center;
|
|
945
|
+
display: inline-flex;
|
|
946
|
+
flex: 0 0 38px;
|
|
947
|
+
justify-content: center;
|
|
948
|
+
width: 38px;
|
|
949
|
+
}
|
|
950
|
+
|
|
924
951
|
.workspace-search {
|
|
925
952
|
background: var(--surface-muted);
|
|
926
953
|
border: 1px solid var(--border);
|
|
@@ -1071,11 +1098,22 @@ code {
|
|
|
1071
1098
|
}
|
|
1072
1099
|
|
|
1073
1100
|
.notification-preview {
|
|
1101
|
+
background: var(--notification-bg, transparent);
|
|
1074
1102
|
border: 1px solid var(--border);
|
|
1075
1103
|
border-left: 4px solid var(--notification-color, var(--accent));
|
|
1076
1104
|
border-radius: 12px;
|
|
1105
|
+
color: var(--text-secondary);
|
|
1106
|
+
cursor: pointer;
|
|
1077
1107
|
gap: 10px;
|
|
1078
1108
|
padding: 10px;
|
|
1109
|
+
text-align: left;
|
|
1110
|
+
width: 100%;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.notification-preview:hover,
|
|
1114
|
+
.notification-preview:focus-visible {
|
|
1115
|
+
border-color: var(--notification-color, var(--accent));
|
|
1116
|
+
outline: none;
|
|
1079
1117
|
}
|
|
1080
1118
|
|
|
1081
1119
|
.notification-preview > span {
|
|
@@ -1095,29 +1133,29 @@ code {
|
|
|
1095
1133
|
|
|
1096
1134
|
.notification-preview.error,
|
|
1097
1135
|
.github-notification-card.error {
|
|
1098
|
-
--notification-color:
|
|
1099
|
-
--notification-bg:
|
|
1136
|
+
--notification-color: var(--notification-error-fg);
|
|
1137
|
+
--notification-bg: var(--notification-error-bg);
|
|
1100
1138
|
}
|
|
1101
1139
|
|
|
1102
1140
|
.notification-preview.warning,
|
|
1103
1141
|
.github-notification-card.warning {
|
|
1104
|
-
--notification-color:
|
|
1105
|
-
--notification-bg:
|
|
1142
|
+
--notification-color: var(--notification-warning-fg);
|
|
1143
|
+
--notification-bg: var(--notification-warning-bg);
|
|
1106
1144
|
}
|
|
1107
1145
|
|
|
1108
1146
|
.notification-preview.info,
|
|
1109
1147
|
.github-notification-card.info {
|
|
1110
|
-
--notification-color:
|
|
1111
|
-
--notification-bg:
|
|
1148
|
+
--notification-color: var(--notification-info-fg);
|
|
1149
|
+
--notification-bg: var(--notification-info-bg);
|
|
1112
1150
|
}
|
|
1113
1151
|
|
|
1114
1152
|
.notification-preview.success,
|
|
1115
1153
|
.github-notification-card.success {
|
|
1116
|
-
--notification-color:
|
|
1117
|
-
--notification-bg:
|
|
1154
|
+
--notification-color: var(--notification-success-fg);
|
|
1155
|
+
--notification-bg: var(--notification-success-bg);
|
|
1118
1156
|
}
|
|
1119
1157
|
|
|
1120
|
-
.popover-menu button,
|
|
1158
|
+
.popover-menu button:not(.notification-preview),
|
|
1121
1159
|
.popover-menu a {
|
|
1122
1160
|
align-items: center;
|
|
1123
1161
|
background: transparent;
|
|
@@ -1131,9 +1169,9 @@ code {
|
|
|
1131
1169
|
text-decoration: none;
|
|
1132
1170
|
}
|
|
1133
1171
|
|
|
1134
|
-
.popover-menu button:hover:not(:disabled),
|
|
1172
|
+
.popover-menu button:not(.notification-preview):hover:not(:disabled),
|
|
1135
1173
|
.popover-menu a:hover,
|
|
1136
|
-
.popover-menu button:focus-visible,
|
|
1174
|
+
.popover-menu button:not(.notification-preview):focus-visible,
|
|
1137
1175
|
.popover-menu a:focus-visible {
|
|
1138
1176
|
background: var(--surface-muted);
|
|
1139
1177
|
color: var(--accent);
|
|
@@ -1966,7 +2004,12 @@ code {
|
|
|
1966
2004
|
|
|
1967
2005
|
.apps-surface {
|
|
1968
2006
|
display: grid;
|
|
1969
|
-
|
|
2007
|
+
align-content: start;
|
|
2008
|
+
gap: 10px;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
.apps-surface .section-heading {
|
|
2012
|
+
margin-bottom: 8px;
|
|
1970
2013
|
}
|
|
1971
2014
|
|
|
1972
2015
|
.data-table,
|
|
@@ -1990,13 +2033,21 @@ code {
|
|
|
1990
2033
|
border: 1px solid var(--border);
|
|
1991
2034
|
border-radius: 18px;
|
|
1992
2035
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
|
|
1993
|
-
overflow:
|
|
2036
|
+
overflow: hidden;
|
|
2037
|
+
transition: background 140ms ease, border-color 140ms ease;
|
|
1994
2038
|
}
|
|
1995
2039
|
|
|
1996
2040
|
.managed-app-card.expanded {
|
|
1997
2041
|
border-color: color-mix(in srgb, var(--accent) 32%, var(--border));
|
|
1998
2042
|
}
|
|
1999
2043
|
|
|
2044
|
+
.managed-app-card:hover,
|
|
2045
|
+
.managed-app-card:focus-within {
|
|
2046
|
+
background:
|
|
2047
|
+
linear-gradient(135deg, color-mix(in srgb, var(--accent) 11%, transparent), transparent 40%),
|
|
2048
|
+
var(--surface-muted);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2000
2051
|
.managed-app-summary {
|
|
2001
2052
|
align-items: center;
|
|
2002
2053
|
background: transparent;
|
|
@@ -2006,6 +2057,7 @@ code {
|
|
|
2006
2057
|
gap: 16px;
|
|
2007
2058
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
2008
2059
|
padding: 16px;
|
|
2060
|
+
position: relative;
|
|
2009
2061
|
text-align: left;
|
|
2010
2062
|
width: 100%;
|
|
2011
2063
|
}
|
|
@@ -2089,6 +2141,7 @@ code {
|
|
|
2089
2141
|
display: grid;
|
|
2090
2142
|
gap: 14px;
|
|
2091
2143
|
padding: 0 16px 16px;
|
|
2144
|
+
position: relative;
|
|
2092
2145
|
}
|
|
2093
2146
|
|
|
2094
2147
|
.managed-app-toolbar {
|
|
@@ -2116,7 +2169,7 @@ code {
|
|
|
2116
2169
|
align-items: center;
|
|
2117
2170
|
color: var(--text-secondary);
|
|
2118
2171
|
display: inline-flex;
|
|
2119
|
-
font-size: 0.
|
|
2172
|
+
font-size: 0.95em;
|
|
2120
2173
|
font-weight: 800;
|
|
2121
2174
|
gap: 4px;
|
|
2122
2175
|
max-width: 100%;
|
|
@@ -2167,6 +2220,11 @@ button.managed-toggle {
|
|
|
2167
2220
|
text-align: left;
|
|
2168
2221
|
}
|
|
2169
2222
|
|
|
2223
|
+
button.managed-toggle:disabled {
|
|
2224
|
+
cursor: not-allowed;
|
|
2225
|
+
opacity: 0.82;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2170
2228
|
button.managed-toggle:hover,
|
|
2171
2229
|
button.managed-toggle:focus-visible {
|
|
2172
2230
|
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
|
@@ -2363,7 +2421,7 @@ button.managed-toggle:focus-visible {
|
|
|
2363
2421
|
content: attr(data-tooltip);
|
|
2364
2422
|
font-size: 0.72rem;
|
|
2365
2423
|
font-weight: 700;
|
|
2366
|
-
left: 50
|
|
2424
|
+
left: clamp(10px, 50%, calc(100vw - 330px));
|
|
2367
2425
|
line-height: 1.35;
|
|
2368
2426
|
max-width: min(320px, 72vw);
|
|
2369
2427
|
opacity: 0;
|
|
@@ -2374,7 +2432,7 @@ button.managed-toggle:focus-visible {
|
|
|
2374
2432
|
transform: translate(-50%, 4px);
|
|
2375
2433
|
transition: opacity 140ms ease, transform 140ms ease;
|
|
2376
2434
|
width: max-content;
|
|
2377
|
-
z-index:
|
|
2435
|
+
z-index: 400;
|
|
2378
2436
|
}
|
|
2379
2437
|
|
|
2380
2438
|
[data-tooltip]:hover::after,
|
|
@@ -2453,14 +2511,22 @@ button.managed-toggle:focus-visible {
|
|
|
2453
2511
|
background: var(--surface-muted);
|
|
2454
2512
|
border: 1px solid var(--border);
|
|
2455
2513
|
border-radius: 999px;
|
|
2456
|
-
|
|
2514
|
+
align-items: center;
|
|
2515
|
+
display: inline-flex;
|
|
2457
2516
|
gap: 4px;
|
|
2458
2517
|
padding: 5px;
|
|
2459
|
-
width:
|
|
2518
|
+
width: max-content;
|
|
2519
|
+
max-width: 100%;
|
|
2460
2520
|
}
|
|
2461
2521
|
|
|
2462
2522
|
.app-subnav {
|
|
2463
|
-
|
|
2523
|
+
justify-self: start;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
.recommended-filter-tabs {
|
|
2527
|
+
display: flex;
|
|
2528
|
+
flex-wrap: wrap;
|
|
2529
|
+
gap: 10px;
|
|
2464
2530
|
}
|
|
2465
2531
|
|
|
2466
2532
|
.recommended-app-grid {
|
|
@@ -2495,19 +2561,38 @@ button.managed-toggle:focus-visible {
|
|
|
2495
2561
|
gap: 6px;
|
|
2496
2562
|
}
|
|
2497
2563
|
|
|
2498
|
-
.
|
|
2564
|
+
.app-icon-groups {
|
|
2565
|
+
display: flex;
|
|
2566
|
+
flex-wrap: wrap;
|
|
2567
|
+
gap: 10px;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
.os-icon-list span,
|
|
2571
|
+
.os-icon-list button {
|
|
2499
2572
|
align-items: center;
|
|
2500
|
-
background:
|
|
2501
|
-
border:
|
|
2502
|
-
border-radius:
|
|
2573
|
+
background: transparent;
|
|
2574
|
+
border: 0;
|
|
2575
|
+
border-radius: 0;
|
|
2503
2576
|
color: var(--text-secondary);
|
|
2504
2577
|
display: inline-flex;
|
|
2505
|
-
font-size:
|
|
2578
|
+
font-size: 1.12rem;
|
|
2506
2579
|
font-weight: 900;
|
|
2507
2580
|
justify-content: center;
|
|
2508
|
-
min-height:
|
|
2509
|
-
min-width:
|
|
2510
|
-
padding:
|
|
2581
|
+
min-height: 24px;
|
|
2582
|
+
min-width: 24px;
|
|
2583
|
+
padding: 0;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
.platform-icon-list button {
|
|
2587
|
+
cursor: pointer;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.os-icon-list span:hover,
|
|
2591
|
+
.os-icon-list span:focus-visible,
|
|
2592
|
+
.os-icon-list button:hover,
|
|
2593
|
+
.os-icon-list button:focus-visible {
|
|
2594
|
+
color: var(--accent);
|
|
2595
|
+
outline: none;
|
|
2511
2596
|
}
|
|
2512
2597
|
|
|
2513
2598
|
.pill-tabs button {
|
|
@@ -2515,20 +2600,30 @@ button.managed-toggle:focus-visible {
|
|
|
2515
2600
|
border: 0;
|
|
2516
2601
|
border-radius: 999px;
|
|
2517
2602
|
color: var(--text-secondary);
|
|
2518
|
-
flex:
|
|
2603
|
+
flex: 0 0 auto;
|
|
2519
2604
|
font-weight: 800;
|
|
2520
2605
|
min-height: 38px;
|
|
2521
2606
|
padding: 9px 14px;
|
|
2522
2607
|
}
|
|
2523
2608
|
|
|
2524
|
-
.pill-tabs button.active
|
|
2609
|
+
.pill-tabs button.active {
|
|
2610
|
+
background: var(--accent-aaa-bg);
|
|
2611
|
+
color: #ffffff;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2525
2614
|
.pill-tabs button:hover,
|
|
2526
2615
|
.pill-tabs button:focus-visible {
|
|
2527
|
-
background: var(--surface-elevated);
|
|
2528
|
-
color: var(--
|
|
2616
|
+
background: color-mix(in srgb, var(--accent) 16%, var(--surface-elevated));
|
|
2617
|
+
color: var(--accent);
|
|
2529
2618
|
outline: none;
|
|
2530
2619
|
}
|
|
2531
2620
|
|
|
2621
|
+
.pill-tabs button.active:hover,
|
|
2622
|
+
.pill-tabs button.active:focus-visible {
|
|
2623
|
+
background: var(--accent-aaa-bg);
|
|
2624
|
+
color: #ffffff;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2532
2627
|
.count-pill,
|
|
2533
2628
|
.repo-summary-strip span {
|
|
2534
2629
|
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
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.12
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|