imcp 0.0.12 → 0.0.13

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.
Files changed (44) hide show
  1. package/dist/core/ConfigurationProvider.d.ts +2 -1
  2. package/dist/core/ConfigurationProvider.js +20 -24
  3. package/dist/core/InstallationService.d.ts +17 -0
  4. package/dist/core/InstallationService.js +127 -61
  5. package/dist/core/MCPManager.d.ts +1 -0
  6. package/dist/core/MCPManager.js +3 -0
  7. package/dist/core/RequirementService.d.ts +4 -4
  8. package/dist/core/RequirementService.js +11 -7
  9. package/dist/core/ServerSchemaProvider.d.ts +1 -1
  10. package/dist/core/ServerSchemaProvider.js +15 -10
  11. package/dist/core/constants.d.ts +3 -0
  12. package/dist/core/constants.js +4 -1
  13. package/dist/core/installers/requirements/PipInstaller.js +10 -5
  14. package/dist/core/types.d.ts +4 -0
  15. package/dist/services/ServerService.d.ts +5 -0
  16. package/dist/services/ServerService.js +15 -0
  17. package/dist/utils/githubAuth.js +0 -10
  18. package/dist/utils/githubUtils.d.ts +16 -0
  19. package/dist/utils/githubUtils.js +55 -39
  20. package/dist/web/public/css/detailsWidget.css +157 -32
  21. package/dist/web/public/css/serverDetails.css +35 -19
  22. package/dist/web/public/index.html +2 -0
  23. package/dist/web/public/js/detailsWidget.js +43 -40
  24. package/dist/web/public/js/serverCategoryDetails.js +182 -120
  25. package/dist/web/server.js +25 -0
  26. package/package.json +3 -4
  27. package/src/core/ConfigurationProvider.ts +37 -29
  28. package/src/core/InstallationService.ts +176 -62
  29. package/src/core/MCPManager.ts +4 -0
  30. package/src/core/RequirementService.ts +12 -8
  31. package/src/core/ServerSchemaLoader.ts +48 -0
  32. package/src/core/ServerSchemaProvider.ts +137 -0
  33. package/src/core/constants.ts +4 -1
  34. package/src/core/installers/requirements/PipInstaller.ts +10 -5
  35. package/src/core/types.ts +4 -0
  36. package/src/services/ServerService.ts +15 -0
  37. package/src/utils/githubAuth.ts +14 -27
  38. package/src/utils/githubUtils.ts +84 -47
  39. package/src/web/public/css/detailsWidget.css +235 -0
  40. package/src/web/public/css/serverDetails.css +126 -0
  41. package/src/web/public/index.html +2 -0
  42. package/src/web/public/js/detailsWidget.js +264 -0
  43. package/src/web/public/js/serverCategoryDetails.js +182 -120
  44. package/src/web/server.ts +31 -0
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { Logger } from '../utils/logger.js';
4
+ import { getServerSchemaProvider } from '../core/ServerSchemaProvider.js';
4
5
  import { mcpManager } from '../core/MCPManager.js';
5
6
  import { UPDATE_CHECK_INTERVAL_MS } from '../core/constants.js';
6
7
  import { updateCheckTracker } from '../utils/UpdateCheckTracker.js';
@@ -96,6 +97,20 @@ export class ServerService {
96
97
  }
97
98
  }
98
99
  }
100
+ /**
101
+ * Gets the schema for a specific server in a category
102
+ */
103
+ async getServerSchema(categoryName, serverName) {
104
+ try {
105
+ const provider = await getServerSchemaProvider();
106
+ const serverMcpConfig = await mcpManager.getServerMcpConfig(categoryName, serverName);
107
+ return await provider.getSchema(categoryName, serverMcpConfig?.schemas || `${serverName}.json`);
108
+ }
109
+ catch (error) {
110
+ Logger.error(`Failed to get schema for server ${serverName} in category ${categoryName}:`, error);
111
+ throw error;
112
+ }
113
+ }
99
114
  /**
100
115
  * Installs a specific mcp tool for a server.
101
116
  * TODO: This might require enhancing MCPManager to handle category-specific installs.
@@ -29,16 +29,6 @@ class GithubAuthError extends Error {
29
29
  export async function checkGithubAuth() {
30
30
  Logger.debug('Starting GitHub authentication check');
31
31
  try {
32
- // Check if git is installed
33
- if (!await isToolInstalled('git')) {
34
- Logger.log('Installing required Git...');
35
- await installCLI('git');
36
- // Verify git was installed correctly, with retry mechanism
37
- if (!await isToolInstalled('git')) {
38
- throw new Error('Failed to install Git. Please install it manually and try again.');
39
- }
40
- Logger.debug('Git installed successfully and verified');
41
- }
42
32
  // Check if gh CLI is installed
43
33
  if (!await isToolInstalled('gh')) {
44
34
  Logger.log('Installing required GitHub CLI...');
@@ -1,4 +1,19 @@
1
1
  import { RegistryConfig, RequirementConfig } from '../core/types.js';
2
+ interface DownloadGithubReleaseResult {
3
+ version: string;
4
+ downloadPath: string;
5
+ }
6
+ /**
7
+ * Downloads a GitHub release asset
8
+ * @param repo GitHub repository in format owner/repo
9
+ * @param version Version to download, can be "latest"
10
+ * @param assetsName Assets name pattern (optional, but either assetsName or assetName must be provided)
11
+ * @param assetName Asset name pattern (optional, but either assetsName or assetName must be provided)
12
+ * @param isFolder Whether to treat the downloaded asset as a folder (default: false)
13
+ * @param targetDirectory Target directory for downloads (default: SETTINGS_DIR/downloads)
14
+ * @returns Object containing version and download path
15
+ */
16
+ export declare function downloadGithubRelease(repo: string, version: string, assetsName?: string, assetName?: string, isFolder?: boolean, targetDirectory?: string): Promise<DownloadGithubReleaseResult>;
2
17
  /**
3
18
  * Helper to handle GitHub release downloads
4
19
  * @param requirement The requirement configuration
@@ -9,3 +24,4 @@ export declare function handleGitHubRelease(requirement: RequirementConfig, regi
9
24
  resolvedVersion: string;
10
25
  resolvedPath: string;
11
26
  }>;
27
+ export {};
@@ -3,78 +3,94 @@ import util from 'util';
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
5
  import { extractZipFile } from './clientUtils.js';
6
- import { Logger } from './logger.js';
7
6
  import { SETTINGS_DIR } from '../core/constants.js';
8
7
  const execAsync = util.promisify(exec);
9
8
  /**
10
- * Helper to handle GitHub release downloads
11
- * @param requirement The requirement configuration
12
- * @param registry The GitHub release registry configuration
13
- * @returns The path to the downloaded file
9
+ * Downloads a GitHub release asset
10
+ * @param repo GitHub repository in format owner/repo
11
+ * @param version Version to download, can be "latest"
12
+ * @param assetsName Assets name pattern (optional, but either assetsName or assetName must be provided)
13
+ * @param assetName Asset name pattern (optional, but either assetsName or assetName must be provided)
14
+ * @param isFolder Whether to treat the downloaded asset as a folder (default: false)
15
+ * @param targetDirectory Target directory for downloads (default: SETTINGS_DIR/downloads)
16
+ * @returns Object containing version and download path
14
17
  */
15
- export async function handleGitHubRelease(requirement, registry) {
16
- if (!registry) {
17
- throw new Error('GitHub release registry configuration is required');
18
+ export async function downloadGithubRelease(repo, version, assetsName, assetName, isFolder = false, targetDirectory) {
19
+ if (!repo) {
20
+ throw new Error('GitHub repository is required');
18
21
  }
19
- const downloadsDir = path.join(SETTINGS_DIR, 'downloads');
20
- await fs.mkdir(downloadsDir, { recursive: true });
21
- const { repository, assetsName, assetName } = registry;
22
- if (!repository) {
23
- throw new Error('GitHub repository is required for GitHub release downloads');
22
+ if (!assetsName && !assetName) {
23
+ throw new Error('Either assetsName or assetName must be specified');
24
24
  }
25
- let version = requirement.version;
26
- let resolvedAssetName = assetName || '';
27
- let resolvedAssetsName = assetsName || '';
28
- const { stdout } = await execAsync(`gh release view --repo ${repository} --json tagName --jq .tagName`);
25
+ const downloadsDir = targetDirectory || path.join(SETTINGS_DIR, 'downloads');
26
+ await fs.mkdir(downloadsDir, { recursive: true });
27
+ // Get latest version if needed
28
+ const { stdout } = await execAsync(`gh release view --repo ${repo} --json tagName --jq .tagName`);
29
29
  const latestTag = stdout.trim();
30
30
  let latestVersion = latestTag;
31
31
  const tagWithVPrefix = latestVersion.startsWith('v');
32
32
  if (tagWithVPrefix)
33
33
  latestVersion = latestVersion.substring(1); // Remove 'v' prefix if present
34
- version = version.includes("latest") ? latestVersion : version;
34
+ const resolvedVersion = version.includes("latest") ? latestVersion : version;
35
+ // Resolve asset names
36
+ let resolvedAssetsName = '';
37
+ let resolvedAssetName = '';
35
38
  if (assetsName) {
36
- resolvedAssetsName = assetsName.replace('${latest}', version).replace('${version}', version);
39
+ resolvedAssetsName = assetsName.replace('${latest}', resolvedVersion).replace('${version}', resolvedVersion);
37
40
  }
38
41
  if (assetName) {
39
- resolvedAssetName = assetName.replace('${latest}', version).replace('${version}', version);
42
+ resolvedAssetName = assetName.replace('${latest}', resolvedVersion).replace('${version}', resolvedVersion);
40
43
  }
41
- Logger.debug(`Downloading ${requirement.name} from GitHub release ${repository} version ${version}`);
42
- Logger.debug(`ResolvedAssetsName} ${resolvedAssetName}; ResolvedAsetName} ${resolvedAssetName}`);
43
- const pattern = resolvedAssetsName ? resolvedAssetsName : resolvedAssetName;
44
- Logger.debug(`Resolved pattern: ${pattern}`);
45
- if (!pattern) {
46
- throw new Error('Either assetsName or assetName must be specified for GitHub release downloads');
44
+ // Validate zip requirement for isFolder
45
+ const pattern = resolvedAssetsName || resolvedAssetName;
46
+ if (isFolder && (!resolvedAssetsName || !resolvedAssetsName.endsWith('.zip'))) {
47
+ throw new Error('When isFolder is true, assetsName must be provided and end with .zip');
47
48
  }
48
49
  // Download the release asset
49
50
  const downloadPath = path.join(downloadsDir, path.basename(pattern));
50
51
  if (!await fileExists(downloadPath)) {
51
- await execAsync(`gh release download ${tagWithVPrefix ? `v${version}` : version} --repo ${repository} --pattern "${pattern}" -O "${downloadPath}"`);
52
+ await execAsync(`gh release download ${tagWithVPrefix ? `v${resolvedVersion}` : resolvedVersion} --repo ${repo} --pattern "${pattern}" -O "${downloadPath}"`);
52
53
  }
53
- // Handle zip file extraction if the downloaded file is a zip
54
- if (downloadPath.endsWith('.zip')) {
54
+ // Handle zip extraction if needed
55
+ if (isFolder && downloadPath.endsWith('.zip')) {
55
56
  const extractDir = path.join(downloadsDir, path.basename(pattern, '.zip'));
56
57
  await fs.mkdir(extractDir, { recursive: true });
57
- // Extract the zip file
58
58
  await extractZipFile(downloadPath, { dir: extractDir });
59
- let assetPath = '';
60
59
  // If resolvedAssetName is specified, look for it in the extracted directory
61
60
  if (resolvedAssetName) {
62
- assetPath = path.join(extractDir, resolvedAssetName);
61
+ const assetPath = path.join(extractDir, resolvedAssetName);
63
62
  try {
64
63
  await fs.access(assetPath);
65
- return { resolvedVersion: version, resolvedPath: assetPath };
64
+ return { version: resolvedVersion, downloadPath: assetPath };
66
65
  }
67
66
  catch (error) {
68
67
  throw new Error(`Asset ${resolvedAssetName} not found in extracted directory ${extractDir}`);
69
68
  }
70
69
  }
71
- else {
72
- assetPath = path.join(extractDir, path.basename(pattern, '.zip') + '.tgz');
73
- }
74
- // If no specific asset is required, return the extraction directory
75
- return { resolvedVersion: version, resolvedPath: extractDir };
70
+ return { version: resolvedVersion, downloadPath: extractDir };
71
+ }
72
+ return { version: resolvedVersion, downloadPath };
73
+ }
74
+ /**
75
+ * Helper to handle GitHub release downloads
76
+ * @param requirement The requirement configuration
77
+ * @param registry The GitHub release registry configuration
78
+ * @returns The path to the downloaded file
79
+ */
80
+ export async function handleGitHubRelease(requirement, registry) {
81
+ if (!registry) {
82
+ throw new Error('GitHub release registry configuration is required');
83
+ }
84
+ const { repository, assetsName, assetName } = registry;
85
+ if (!repository) {
86
+ throw new Error('GitHub repository is required for GitHub release downloads');
76
87
  }
77
- return { resolvedVersion: version, resolvedPath: downloadPath };
88
+ const isZipAsset = assetsName?.endsWith('.zip') || false;
89
+ const result = await downloadGithubRelease(repository, requirement.version, assetsName, assetName, isZipAsset);
90
+ return {
91
+ resolvedVersion: result.version,
92
+ resolvedPath: result.downloadPath
93
+ };
78
94
  }
79
95
  async function fileExists(filePath) {
80
96
  try {
@@ -1,30 +1,75 @@
1
1
  .details-widget {
2
2
  transition: all 0.3s ease-in-out;
3
+ width: 100%;
4
+ max-width: 100%;
5
+ box-sizing: border-box;
6
+ margin: 0;
7
+ padding: 0;
8
+ display: block;
9
+ overflow: hidden;
3
10
  }
4
11
 
5
- .tools-grid {
6
- display: grid;
7
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
8
- gap: 0.5rem;
9
- padding: 0.5rem;
12
+ .tools-list {
13
+ width: 100%;
14
+ max-width: 100%;
15
+ margin: 0;
16
+ padding: 0.1rem;
17
+ box-sizing: border-box;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .tool-card {
22
+ width: 100%;
23
+ margin-bottom: -1px;
24
+ border-radius: 0;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ .tool-card:first-child {
29
+ border-top-left-radius: 6px;
30
+ border-top-right-radius: 6px;
31
+ }
32
+
33
+ .tool-card:last-child {
34
+ border-bottom-left-radius: 6px;
35
+ border-bottom-right-radius: 6px;
36
+ margin-bottom: 0;
10
37
  }
11
38
 
12
39
  .tool-card {
13
40
  transition: all 0.3s ease-out;
14
41
  border: 1px solid #e5e7eb;
15
- margin-bottom: 0.5rem;
16
- padding: 0.75rem;
42
+ padding: 0.5rem;
43
+ background-color: white;
44
+ position: relative;
45
+ z-index: 1;
46
+ font-size: 0.9rem;
47
+ width: 100%;
48
+ box-sizing: border-box;
49
+ }
50
+
51
+ .tool-card .text-gray-600 {
52
+ font-size: 0.75rem;
53
+ line-height: 1.3;
17
54
  }
18
55
 
19
56
  .tool-card.active {
20
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
21
- border-left: 4px solid #3b82f6;
57
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
58
+ border-color: transparent;
22
59
  background-color: #f8fafc;
23
60
  }
24
61
 
62
+ .tool-card:hover {
63
+ transform: translateY(-1px);
64
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
65
+ }
66
+
25
67
  .tool-card-header {
26
68
  position: relative;
27
69
  padding-right: 2rem;
70
+ width: 100%;
71
+ box-sizing: border-box;
72
+ cursor: pointer;
28
73
  }
29
74
 
30
75
  .tool-card-header::after {
@@ -49,23 +94,33 @@
49
94
  max-height: 0;
50
95
  opacity: 0;
51
96
  overflow: hidden;
52
- transition: all 0.3s ease-out;
97
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
98
+ background-color: #f9fafb;
99
+ border-radius: 6px;
100
+ width: 100%;
101
+ max-width: 100%;
102
+ box-sizing: border-box;
53
103
  }
54
104
 
55
105
  .tool-details.visible {
56
- max-height: 1500px;
106
+ max-height: 2000px;
57
107
  opacity: 1;
58
- padding-top: 0.75rem;
59
- margin-top: 0.75rem;
108
+ padding: 0.1rem;
109
+ margin-top: 0.5rem;
60
110
  border-top: 1px solid #e5e7eb;
111
+ width: 100%;
112
+ max-width: 100%;
113
+ box-sizing: border-box;
61
114
  }
62
115
 
63
116
  .property-item {
64
- margin-bottom: 0.75rem;
65
- padding-left: 0.75rem;
117
+ margin-bottom: 0.15rem;
118
+ padding: 0.5rem;
66
119
  border-left: 2px solid #e5e7eb;
67
- transition: border-color 0.2s ease;
68
- font-size: 0.9rem;
120
+ transition: all 0.2s ease;
121
+ font-size: 0.8rem;
122
+ background-color: white;
123
+ border-radius: 4px;
69
124
  }
70
125
 
71
126
  .property-item:hover {
@@ -73,38 +128,108 @@
73
128
  }
74
129
 
75
130
  .property-header {
76
- margin-bottom: 0.5rem;
131
+ margin-bottom: 0.25rem;
132
+ }
133
+
134
+ .property-title {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.5rem;
138
+ flex-wrap: wrap;
77
139
  }
78
140
 
79
141
  .property-name {
80
142
  font-weight: 600;
81
143
  color: #1e293b;
144
+ font-size: 0.85rem;
145
+ background-color: #f8fafc;
146
+ padding: 0.2rem 0.4rem;
147
+ border-radius: 4px;
148
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
149
+ display: inline-block;
82
150
  }
83
151
 
84
152
  .property-type {
85
153
  color: #64748b;
86
- font-size: 0.875rem;
154
+ font-size: 0.65rem;
155
+ padding: 0.1rem 0.3rem;
156
+ background: #f1f5f9;
157
+ border-radius: 3px;
158
+ font-family: 'Courier New', monospace;
159
+ margin-left: 0.3rem;
87
160
  }
88
161
 
89
162
  .property-desc {
90
- color: #475569;
91
- font-size: 0.8125rem;
92
- line-height: 1.4;
93
- margin-top: 0.25rem;
163
+ color: #6b7280;
164
+ font-size: 0.7rem;
165
+ margin-left: 0.5rem;
166
+ display: inline-block;
167
+ font-style: italic;
168
+ }
169
+
170
+ .property-default {
171
+ font-size: 0.75rem;
172
+ color: #6b7280;
173
+ margin-top: 0.15rem;
174
+ }
175
+
176
+ .property-default code {
177
+ background: #f1f5f9;
178
+ padding: 0.2rem 0.4rem;
179
+ border-radius: 4px;
180
+ font-family: 'Courier New', monospace;
181
+ font-size: 0.85rem;
94
182
  }
95
183
 
96
184
  .required-fields {
97
- background-color: #fef3c7;
98
- border-left: 4px solid #f59e0b;
99
- padding: 0.5rem 0.75rem;
100
- margin-bottom: 1rem;
101
- font-size: 0.875rem;
185
+ border-left: 2px solid #64748b;
186
+ padding: 0.25rem 0.5rem;
187
+ margin-bottom: 0.5rem;
188
+ font-size: 0.8rem;
189
+ border-radius: 2px;
190
+ color: #64748b;
191
+ }
192
+ .required-star {
193
+ color: #dc2626;
194
+ margin-left: 2px;
195
+ font-weight: bold;
196
+ }
197
+
198
+
199
+ /* Ensure proper container behavior for the widget */
200
+ .details-widget-container {
201
+ width: 100%;
202
+ max-width: 100%;
203
+ box-sizing: border-box;
204
+ position: relative;
205
+ margin: 0;
206
+ padding: 0;
207
+ overflow: hidden;
102
208
  }
103
209
 
104
210
  .nested-properties {
105
- margin-left: 0.75rem;
106
- padding-left: 0.75rem;
211
+ margin: 0.25rem 0 0.25rem 0.5rem;
212
+ padding-left: 0.5rem;
107
213
  border-left: 1px solid #e5e7eb;
108
- margin-top: 0.5rem;
109
- font-size: 0.875rem;
214
+ font-size: 0.8rem;
215
+ }
216
+
217
+ .nested-property-item {
218
+ margin-bottom: 0.5rem;
219
+ padding: 0.25rem 0.5rem;
220
+ }
221
+
222
+ .nested-property-item .property-name {
223
+ font-size: 0.85rem;
224
+ }
225
+
226
+ .nested-property-item .property-type {
227
+ font-size: 0.8rem;
228
+ padding: 0.1rem 0.3rem;
229
+ }
230
+
231
+ .nested-property-item .property-desc {
232
+ font-size: 0.8rem;
233
+ margin-top: 0.25rem;
234
+ color: #64748b;
110
235
  }
@@ -1,24 +1,24 @@
1
1
  /* Server item container */
2
- .server-item {
2
+ .server-item-content {
3
3
  cursor: pointer;
4
4
  position: relative;
5
5
  transition: all 0.2s ease;
6
- }
7
-
8
- .server-item-content {
9
- position: relative;
10
6
  border: 1px solid #e5e7eb;
11
7
  border-radius: 0.5rem;
12
8
  padding: 1rem;
13
9
  padding-right: calc(120px + 3rem); /* Button width + spacing */
14
- margin-bottom: 1rem;
10
+ box-sizing: border-box;
15
11
  background-color: #ffffff;
16
- transition: all 0.2s ease;
12
+ z-index: 1;
13
+ margin-bottom: 1rem;
14
+ width: 100%;
15
+ max-width: 100%;
16
+ overflow: visible;
17
17
  }
18
18
 
19
- .server-item:hover .server-item-content {
20
- border-color: #3b82f6;
21
- box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1);
19
+ .server-item-content:hover {
20
+ border-color: transparent;
21
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
22
22
  transform: translateY(-1px);
23
23
  }
24
24
 
@@ -29,20 +29,33 @@
29
29
  transition: max-height 0.3s ease-out;
30
30
  background-color: #f8fafc;
31
31
  border-radius: 0 0 0.5rem 0.5rem;
32
- margin-top: -1rem;
33
- margin-bottom: 1rem;
32
+ margin: -1px 0 0;
34
33
  border: 1px solid #e5e7eb;
35
34
  border-top: none;
35
+ position: relative;
36
+ z-index: 0;
37
+ width: 100%;
38
+ max-width: 100%;
39
+ box-sizing: border-box;
40
+ left: 0;
41
+ right: 0;
36
42
  }
37
43
 
38
44
  .details-widget.expanded {
39
- max-height: 800px; /* Increased height to accommodate more content */
40
- border-color: #3b82f6;
41
- transition: max-height 0.3s ease-in-out;
45
+ max-height: 2000px; /* Increased height to accommodate more content */
46
+ border-color: transparent;
47
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
48
+ transition: max-height 0.3s ease-in-out, box-shadow 0.2s ease;
49
+ width: 100%;
50
+ margin-left: 0;
51
+ margin-right: 0;
52
+ display: block;
42
53
  }
43
54
 
44
55
  .details-widget-content {
45
56
  padding: 1rem;
57
+ width: 100%;
58
+ box-sizing: border-box;
46
59
  }
47
60
 
48
61
  .description-text {
@@ -53,9 +66,12 @@
53
66
 
54
67
  /* Expand/collapse animation */
55
68
  .server-item-content.expanded {
69
+ border-bottom: none;
56
70
  border-bottom-left-radius: 0;
57
71
  border-bottom-right-radius: 0;
58
- border-color: #3b82f6;
72
+ border-color: transparent;
73
+ box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.08);
74
+ margin-bottom: 0;
59
75
  }
60
76
 
61
77
  /* Server item layout */
@@ -84,13 +100,13 @@
84
100
  .action-buttons {
85
101
  position: absolute;
86
102
  right: 1rem;
87
- top: 50%;
88
- transform: translateY(-50%);
103
+ top: calc(2rem + 0.5rem); /* Align with description text (header height + margin-bottom) */
89
104
  margin: 0;
105
+ z-index: 2; /* Ensure buttons stay on top */
90
106
  }
91
107
 
92
108
  .action-buttons button {
93
- min-width: 120px;
109
+ min-width: 100px;
94
110
  padding: 0.5rem 1.5rem;
95
111
  text-align: center;
96
112
  font-weight: 600;
@@ -12,6 +12,8 @@
12
12
  <link rel="stylesheet" href="styles.css">
13
13
  <link rel="stylesheet" href="css/modal.css">
14
14
  <link rel="stylesheet" href="css/notifications.css">
15
+ <link rel="stylesheet" href="css/serverDetails.css">
16
+ <link rel="stylesheet" href="css/detailsWidget.css">
15
17
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
16
18
 
17
19
  <!-- Alert container for notifications -->