Skip to content

feat: container service link for stacks#131

Merged
kmendell merged 4 commits intomainfrom
feat/service-link
May 8, 2025
Merged

feat: container service link for stacks#131
kmendell merged 4 commits intomainfrom
feat/service-link

Conversation

@kmendell
Copy link
Member

@kmendell kmendell commented May 8, 2025

fixes: #125

Summary by CodeRabbit

  • New Features

    • Added the ability to view detailed information for individual containers.
    • Introduced a new "Base Server URL" input in the app settings for Docker configuration.
    • Enhanced the stack page to display clickable external links for each service port, allowing quick access to exposed services.
  • Improvements

    • Improved the layout and clarity of the services list within the stack page for better usability and presentation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 8, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes introduce a new API endpoint and supporting service method to fetch details for a specific container by ID, add a configurable baseServerUrl setting to the application, and enhance the stack page to display clickable service port links using this configurable address. UI updates include a new input for the base server URL and improved service port presentation.

Changes

File(s) Change Summary
src/lib/services/api/container-api-service.ts Added an asynchronous get(id: string) method to fetch a single container's details by ID from the API.
src/lib/types/settings.type.ts Added an optional baseServerUrl property to the Settings interface.
src/routes/api/containers/[containerId]/+server.ts Introduced a new GET API handler to retrieve container details by containerId, returning 404 if not found and 500 on error.
src/routes/settings/tabs/app-settings.svelte Added a required input field for "Base Server URL" in Docker Settings, bound to settingsStore.baseServerUrl, with descriptive help text.
src/routes/stacks/[stackId]/+page.server.ts Updated the server-side loader to fetch global settings and container details for each service, extracting and formatting service port bindings, and returning them with the page data.
src/routes/stacks/[stackId]/+page.svelte Enhanced the UI to display clickable service port links using the configured server address, added logic to determine the appropriate host for each service, and improved the visual structure of the service list. Introduced getHostForService, getServicePortUrl, and supporting types.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant StackPage (Svelte)
    participant ServerLoader (+page.server.ts)
    participant ContainerAPIService
    participant ContainerAPIEndpoint
    participant SettingsService

    User->>StackPage (Svelte): Visit stack details page
    StackPage->>ServerLoader: load(stackId)
    ServerLoader->>SettingsService: getSettings()
    ServerLoader->>ContainerAPIService: getContainer(serviceId) for each service
    ContainerAPIService->>ContainerAPIEndpoint: GET /containers/{id}
    ContainerAPIEndpoint-->>ContainerAPIService: Container data / 404 / 500
    ContainerAPIService-->>ServerLoader: Container details
    ServerLoader-->>StackPage: stack, editorState, servicePorts, settings
    StackPage->>User: Render service port links using settings.baseServerUrl
Loading

Assessment against linked issues

Objective Addressed Explanation
Add Service Link with Configurable Service Address (#125)
Allow user to configure the base server URL for service links (#125)
Display clickable links for service ports using the configured address on the stack page (#125)
Provide UI for setting the base server URL in application settings (#125)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

}

return json(container);
} catch (error: any) {

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
const parsedPorts: string[] = [];

for (const containerPort in portBindings) {
if (portBindings.hasOwnProperty(containerPort)) {

Check failure

Code scanning / ESLint

Disallow calling some `Object.prototype` methods directly on objects Error

Do not access Object.prototype method 'hasOwnProperty' from target object.
if (portBindings.hasOwnProperty(containerPort)) {
const bindings = portBindings[containerPort];
if (bindings && Array.isArray(bindings) && bindings.length > 0) {
bindings.forEach((binding: any) => {

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
);
}

function getHostForService(service: any): string {

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
}

// Update your getServicePortUrl function to handle different port structures
function getServicePortUrl(service: any, port: string | number | Port, protocol = 'http'): string {

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
Comment on lines +171 to +178
{#each Object.values(servicePorts) as ports}
{#each ports as port}
<a href={getServicePortUrl(stack, port)} target="_blank" rel="noopener noreferrer" class="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 px-2 py-1 rounded-md hover:bg-blue-500/20 transition-colors flex items-center gap-1">
{port}
<ExternalLink class="h-3 w-3" />
</a>
{/each}
{/each}

Check failure

Code scanning / ESLint

require keyed `{#each}` block Error

Each block should have a key
Comment on lines +172 to +177
{#each ports as port}
<a href={getServicePortUrl(stack, port)} target="_blank" rel="noopener noreferrer" class="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 px-2 py-1 rounded-md hover:bg-blue-500/20 transition-colors flex items-center gap-1">
{port}
<ExternalLink class="h-3 w-3" />
</a>
{/each}

Check failure

Code scanning / ESLint

require keyed `{#each}` block Error

Each block should have a key
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9422299 and fa7c507.

📒 Files selected for processing (6)
  • src/lib/services/api/container-api-service.ts (1 hunks)
  • src/lib/types/settings.type.ts (1 hunks)
  • src/routes/api/containers/[containerId]/+server.ts (1 hunks)
  • src/routes/settings/tabs/app-settings.svelte (1 hunks)
  • src/routes/stacks/[stackId]/+page.server.ts (2 hunks)
  • src/routes/stacks/[stackId]/+page.svelte (6 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/routes/stacks/[stackId]/+page.server.ts (3)
src/lib/services/settings-service.ts (1)
  • getSettings (107-138)
src/lib/stores/settings-store.ts (1)
  • getSettings (68-70)
src/lib/services/docker/container-service.ts (1)
  • getContainer (53-78)
src/routes/api/containers/[containerId]/+server.ts (1)
src/lib/services/docker/container-service.ts (1)
  • getContainer (53-78)
🪛 Biome (1.9.4)
src/routes/stacks/[stackId]/+page.server.ts

[error] 30-30: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 35-35: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)

🪛 GitHub Check: ESLint
src/routes/stacks/[stackId]/+page.server.ts

[failure] 35-35: Disallow calling some Object.prototype methods directly on objects
Do not access Object.prototype method 'hasOwnProperty' from target object.


[failure] 38-38: Disallow the any type
Unexpected any. Specify a different type.

src/routes/api/containers/[containerId]/+server.ts

[failure] 16-16: Disallow the any type
Unexpected any. Specify a different type.

src/routes/stacks/[stackId]/+page.svelte

[failure] 83-83: Disallow the any type
Unexpected any. Specify a different type.


[failure] 102-102: Disallow the any type
Unexpected any. Specify a different type.


[failure] 106-106: Disallow unused variables
'PortBinding' is defined but never used.


[failure] 112-112: Disallow the any type
Unexpected any. Specify a different type.


[failure] 171-178: require keyed {#each} block
Each block should have a key


[failure] 172-177: require keyed {#each} block
Each block should have a key

🔇 Additional comments (5)
src/lib/types/settings.type.ts (1)

30-30: LGTM: Added property for container service links.

The addition of the optional baseServerUrl property is properly implemented and supports the functionality for generating links to container services.

src/lib/services/api/container-api-service.ts (1)

49-53: LGTM: New method to fetch a single container.

The get method is well-implemented and follows the same pattern as other methods in the class.

src/routes/api/containers/[containerId]/+server.ts (2)

1-4: LGTM: Proper imports for container API endpoint.

The imports look good, correctly importing the necessary functions and types.


5-14: LGTM: Well-structured container retrieval logic.

The container retrieval logic is implemented correctly with proper error handling for the case when a container is not found.

src/routes/stacks/[stackId]/+page.server.ts (1)

38-41: Avoid any in the inner loop & capture the actual Docker type

binding is typed as any, losing compile-time safety.
Dockerode defines PortBinding as:

type PortBinding = { HostIp?: string; HostPort?: string };

Refine the type and drop an unnecessary split:

-										bindings.forEach((binding: any) => {
+										bindings.forEach((binding: { HostPort?: string }) => {

[ suggest_optional_refactor ]

🧰 Tools
🪛 GitHub Check: ESLint

[failure] 38-38: Disallow the any type
Unexpected any. Specify a different type.

Comment on lines +82 to +86
<div class="space-y-2">
<label for="baseServerUrl" class="text-sm font-medium">Base Server URL</label>
<Input type="text" id="baseServerUrl" name="baseServerUrl" bind:value={$settingsStore.baseServerUrl} placeholder="localhost" required />
<p class="text-xs text-muted-foreground">Host/IP used when accessing container services that aren't on ipvlan or macvlan networks</p>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Required attribute inconsistent with type definition.

The input field is marked as required, but the corresponding baseServerUrl property in the Settings interface is optional (denoted by ?). This inconsistency could cause unexpected behaviors.

-					<Input type="text" id="baseServerUrl" name="baseServerUrl" bind:value={$settingsStore.baseServerUrl} placeholder="localhost" required />
+					<Input type="text" id="baseServerUrl" name="baseServerUrl" bind:value={$settingsStore.baseServerUrl} placeholder="localhost" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="space-y-2">
<label for="baseServerUrl" class="text-sm font-medium">Base Server URL</label>
<Input type="text" id="baseServerUrl" name="baseServerUrl" bind:value={$settingsStore.baseServerUrl} placeholder="localhost" required />
<p class="text-xs text-muted-foreground">Host/IP used when accessing container services that aren't on ipvlan or macvlan networks</p>
</div>
<div class="space-y-2">
<label for="baseServerUrl" class="text-sm font-medium">Base Server URL</label>
<Input
type="text"
id="baseServerUrl"
name="baseServerUrl"
bind:value={$settingsStore.baseServerUrl}
placeholder="localhost"
/>
<p class="text-xs text-muted-foreground">
Host/IP used when accessing container services that aren't on ipvlan or macvlan networks
</p>
</div>
🤖 Prompt for AI Agents (early access)
In src/routes/settings/tabs/app-settings.svelte around lines 82 to 86, the input
field for baseServerUrl is marked as required, but the baseServerUrl property in
the Settings interface is optional. To fix this inconsistency, either remove the
required attribute from the input field if baseServerUrl should be optional, or
update the Settings interface to make baseServerUrl a required property to match
the input field's requirement.

return json({ success: false, error: 'Container not found' }, { status: 404 });
}

return json(container);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider consistent response structures.

The success response structure is different from the error response structure. Consider using a consistent pattern for both cases.

-		return json(container);
+		return json({
+			success: true,
+			data: container
+		});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return json(container);
// ...
// previously:
// return json(container);
return json({
success: true,
data: container
});
// ...
🤖 Prompt for AI Agents (early access)
In src/routes/api/containers/[containerId]/+server.ts at line 15, the success
response returns the container object directly, while error responses likely use
a different structure. To maintain consistency, wrap the success response data
inside a standard response object, for example, { success: true, data: container
}, so that both success and error responses follow a similar pattern.

Comment on lines +16 to +25
} catch (error: any) {
console.error(`API Error getting container ${containerId}:`, error);
return json(
{
success: false,
error: error.message || 'Failed to get container information'
},
{ status: 500 }
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace any type with a more specific error type.

The code uses error: any which is flagged by ESLint. Consider using a more specific type to improve type safety.

-	} catch (error: any) {
+	} catch (error: unknown) {
 		console.error(`API Error getting container ${containerId}:`, error);
+		const errorMessage = error instanceof Error ? error.message : 'Failed to get container information';
 		return json(
 			{
 				success: false,
-				error: error.message || 'Failed to get container information'
+				error: errorMessage
 			},
 			{ status: 500 }
 		);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error: any) {
console.error(`API Error getting container ${containerId}:`, error);
return json(
{
success: false,
error: error.message || 'Failed to get container information'
},
{ status: 500 }
);
}
} catch (error: unknown) {
console.error(`API Error getting container ${containerId}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Failed to get container information';
return json(
{
success: false,
error: errorMessage
},
{ status: 500 }
);
}
🧰 Tools
🪛 GitHub Check: ESLint

[failure] 16-16: Disallow the any type
Unexpected any. Specify a different type.

🤖 Prompt for AI Agents (early access)
In src/routes/api/containers/[containerId]/+server.ts around lines 16 to 25,
replace the catch block's error type from 'any' to a more specific type such as
'unknown'. Then, add a type guard or assertion inside the catch block to safely
access the error message property, ensuring type safety and compliance with
ESLint rules.

Comment on lines +34 to +36
for (const containerPort in portBindings) {
if (portBindings.hasOwnProperty(containerPort)) {
const bindings = portBindings[containerPort];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Replace hasOwnProperty with Object.hasOwn

Directly calling hasOwnProperty on a value breaches the ESLint rule no-prototype-builtins (and Biome’s counterpart).
Using the static helper is both safer and more concise.

-								if (portBindings.hasOwnProperty(containerPort)) {
+								if (Object.hasOwn(portBindings, containerPort)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const containerPort in portBindings) {
if (portBindings.hasOwnProperty(containerPort)) {
const bindings = portBindings[containerPort];
for (const containerPort in portBindings) {
if (Object.hasOwn(portBindings, containerPort)) {
const bindings = portBindings[containerPort];
🧰 Tools
🪛 Biome (1.9.4)

[error] 35-35: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)

🪛 GitHub Check: ESLint

[failure] 35-35: Disallow calling some Object.prototype methods directly on objects
Do not access Object.prototype method 'hasOwnProperty' from target object.

🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.server.ts around lines 34 to 36, replace
the call to portBindings.hasOwnProperty(containerPort) with
Object.hasOwn(portBindings, containerPort) to comply with ESLint's
no-prototype-builtins rule and improve safety and clarity.

Comment on lines +30 to +32
if (containerData && containerData.networkSettings?.Ports) {
const portBindings = containerData.networkSettings.Ports;
const parsedPorts: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Use optional-chaining instead of a double check

containerData && containerData.networkSettings?.Ports can be shortened to a single optional-chain.
The current pattern is harmless but the linter (useOptionalChain) is already flagging it.

-						if (containerData && containerData.networkSettings?.Ports) {
+						if (containerData?.networkSettings?.Ports) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (containerData && containerData.networkSettings?.Ports) {
const portBindings = containerData.networkSettings.Ports;
const parsedPorts: string[] = [];
if (containerData?.networkSettings?.Ports) {
const portBindings = containerData.networkSettings.Ports;
const parsedPorts: string[] = [];
🧰 Tools
🪛 Biome (1.9.4)

[error] 30-30: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.server.ts around lines 30 to 32, replace
the double check 'containerData && containerData.networkSettings?.Ports' with a
single optional chaining expression 'containerData?.networkSettings?.Ports' to
simplify the condition and satisfy the linter rule useOptionalChain.

Comment on lines +97 to +145
// Define a more specific interface for the port type
interface Port {
PublicPort?: number;
PrivatePort?: number;
Type?: string;
[key: string]: any;
}

// Add this interface to your existing interfaces
interface PortBinding {
HostIp?: string;
HostPort?: string;
}

// Update your getServicePortUrl function to handle different port structures
function getServicePortUrl(service: any, port: string | number | Port, protocol = 'http'): string {
const host = getHostForService(service);

// Handle string port format (most common case according to your type definition)
if (typeof port === 'string') {
// Port might be in format like "8080/tcp" or just "8080"
const parts = port.split('/');
const portNumber = parseInt(parts[0], 10);

// If port includes protocol info and it's udp, use https instead of http
if (parts.length > 1 && parts[1] === 'udp') {
protocol = 'https';
}

return `${protocol}://${host}:${portNumber}`;
}

// Handle numeric port
if (typeof port === 'number') {
return `${protocol}://${host}:${port}`;
}

// Handle Port interface (probably won't be used based on your type definition)
if (port && typeof port === 'object') {
const portNumber = port.PublicPort || port.PrivatePort || 80;
if (port.Type) {
protocol = port.Type.toLowerCase() === 'tcp' ? 'http' : 'https';
}
return `${protocol}://${host}:${portNumber}`;
}

// Fallback
return `${protocol}://${host}:80`;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

udphttps – protocol mapping bug

Inside getServicePortUrl, UDP ports are switched to the https scheme:

if (parts.length > 1 && parts[1] === 'udp') {
  protocol = 'https';
}

https is still TCP based. For plain service discovery the safest default is:

  1. Use http when tcp.
  2. Keep udp as protocol label (e.g. udp://host:port) or omit link generation altogether.
-	if (parts.length > 1 && parts[1] === 'udp') {
-		protocol = 'https';
-	}
+	if (parts.length > 1 && parts[1] === 'udp') {
+		// Browsers cannot open raw UDP, simply return the numeric port w/out protocol
+		return `${host}:${portNumber}`;
+	}
🧰 Tools
🪛 GitHub Check: ESLint

[failure] 102-102: Disallow the any type
Unexpected any. Specify a different type.


[failure] 106-106: Disallow unused variables
'PortBinding' is defined but never used.


[failure] 112-112: Disallow the any type
Unexpected any. Specify a different type.

🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.svelte between lines 97 and 145, the
getServicePortUrl function incorrectly maps UDP ports to the https protocol,
which is TCP-based. To fix this, update the logic so that when the port type is
'udp', the URL uses the 'udp://' scheme instead of 'https://'. For TCP ports,
continue using 'http://'. This ensures the protocol in the URL accurately
reflects the port type.

Comment on lines 320 to 346
<div class={`flex flex-col p-3 border rounded-md ${service.id ? 'hover:bg-muted/50 transition-colors' : ''}`}>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="bg-muted rounded-md p-1">
<Layers class="h-4 w-4" />
</div>
<div>
<p class="font-medium flex items-center gap-1">
{service.name}
</p>
<p class="text-xs text-muted-foreground">
{service.id ? service.id.substring(0, 12) : 'Not created'}
</p>
</div>
</div>
<div>
<p class="font-medium">{service.name}</p>
<p class="text-xs text-muted-foreground">
{service.id ? service.id.substring(0, 12) : 'Not created'}
</p>
<div class="flex items-center gap-2">
<StatusBadge {variant} text={capitalizeFirstLetter(status)} />
{#if service.id}
<a href={`/containers/${service.id}`} class="text-xs text-blue-500 ml-2 flex items-center">
<span class="hidden sm:inline">View details</span>
<ArrowRight class="inline-block ml-1 h-3 w-3" />
</a>
{/if}
</div>
</div>
<div class="flex items-center gap-2">
<StatusBadge {variant} text={capitalizeFirstLetter(status)} />
{#if service.id}
<div class="text-xs text-blue-500 ml-2">
<span class="hidden sm:inline">View details</span>
<ArrowRight class="inline-block ml-1 h-3 w-3" />
</div>
{/if}
</div>
</a>
</div>
{/each}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Semantic HTML: swap <div> wrapper for <a> only when ID exists

When service.id is present the entire row is still a non-interactive <div> but the styling implies clickability (hover:bg-muted/50).
Consider conditionally wrapping the whole element in <a> – it improves a11y and enlarges the hit-target:

{#if service.id}
  <a href={`/containers/${service.id}`} class="flex flex-col p-3 border rounded-md hover:bg-muted/50 transition-colors">
   …
  </a>
{:else}
  <div class="flex flex-col p-3 border rounded-md">
   …
  </div>
{/if}
🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.svelte around lines 320 to 346, the outer
wrapper is always a div with hover styling that implies clickability when
service.id exists, but it is not interactive. To fix this, conditionally render
the entire wrapper as an <a> element with the href and hover styles when
service.id is present, and as a plain <div> without hover styles when service.id
is absent. This improves semantic HTML and accessibility by making the whole row
clickable only when appropriate.

Comment on lines +106 to +110
interface PortBinding {
HostIp?: string;
HostPort?: string;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove unused PortBinding interface

The interface is defined but never referenced, causing an ESLint failure.

-interface PortBinding {
-	HostIp?: string;
-	HostPort?: string;
-}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface PortBinding {
HostIp?: string;
HostPort?: string;
}
🧰 Tools
🪛 GitHub Check: ESLint

[failure] 106-106: Disallow unused variables
'PortBinding' is defined but never used.

🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.svelte around lines 106 to 110, the
PortBinding interface is defined but not used anywhere in the code, causing an
ESLint error. Remove the entire PortBinding interface declaration to resolve the
unused code issue.

Comment on lines +168 to +179
<h1 class="text-2xl font-bold tracking-tight flex items-center gap-2">
{stack?.name || 'Stack Details'}
{#if stack && servicePorts && Object.keys(servicePorts).length > 0}
{#each Object.values(servicePorts) as ports}
{#each ports as port}
<a href={getServicePortUrl(stack, port)} target="_blank" rel="noopener noreferrer" class="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 px-2 py-1 rounded-md hover:bg-blue-500/20 transition-colors flex items-center gap-1">
{port}
<ExternalLink class="h-3 w-3" />
</a>
{/each}
{/each}
{/if}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Key the nested {#each} blocks to satisfy the compiler & avoid diff churn

Both loops currently render without keys, triggering an ESLint/Svelte error and risking unstable DOM diffing.

-{#each Object.values(servicePorts) as ports}
-	{#each ports as port}
+{#each Object.entries(servicePorts) as [serviceId, ports] (serviceId)}
+	{#each ports as port (port)}

Using the service ID and the port string guarantees uniqueness.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h1 class="text-2xl font-bold tracking-tight flex items-center gap-2">
{stack?.name || 'Stack Details'}
{#if stack && servicePorts && Object.keys(servicePorts).length > 0}
{#each Object.values(servicePorts) as ports}
{#each ports as port}
<a href={getServicePortUrl(stack, port)} target="_blank" rel="noopener noreferrer" class="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 px-2 py-1 rounded-md hover:bg-blue-500/20 transition-colors flex items-center gap-1">
{port}
<ExternalLink class="h-3 w-3" />
</a>
{/each}
{/each}
{/if}
<h1 class="text-2xl font-bold tracking-tight flex items-center gap-2">
{stack?.name || 'Stack Details'}
{#if stack && servicePorts && Object.keys(servicePorts).length > 0}
- {#each Object.values(servicePorts) as ports}
- {#each ports as port}
+ {#each Object.entries(servicePorts) as [serviceId, ports] (serviceId)}
+ {#each ports as port (port)}
<a href={getServicePortUrl(stack, port)} target="_blank" rel="noopener noreferrer" class="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 px-2 py-1 rounded-md hover:bg-blue-500/20 transition-colors flex items-center gap-1">
{port}
<ExternalLink class="h-3 w-3" />
</a>
{/each}
{/each}
{/if}
🧰 Tools
🪛 GitHub Check: ESLint

[failure] 171-178: require keyed {#each} block
Each block should have a key


[failure] 172-177: require keyed {#each} block
Each block should have a key

🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.svelte around lines 168 to 179, the nested
{#each} blocks rendering servicePorts lack unique keys, causing ESLint/Svelte
errors and unstable DOM updates. Fix this by adding a unique key to each {#each}
block: use the service ID or index for the outer loop and the port string or
index for the inner loop to ensure stable and efficient rendering.

Comment on lines +83 to +96
function getHostForService(service: any): string {
if (!service || !service.networkSettings?.Networks) return baseServerUrl;

const networks = service.networkSettings.Networks;
for (const networkName in networks) {
const network = networks[networkName];
if (network.Driver === 'macvlan' || network.Driver === 'ipvlan') {
if (network.IPAddress) return network.IPAddress;
}
}

return baseServerUrl;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Narrow the service parameter type & drop any

service originates from a Docker inspect call, so at minimum it owns { networkSettings?: … }.
Switching to an interface (or Partial<ContainerInspectInfo>) removes three ESLint “no-any” violations in one go.

-function getHostForService(service: any): string {
+import type { ContainerInfo } from '$lib/types/docker'; // or whatever you export
+
+function getHostForService(service: Partial<ContainerInfo>): string {

If the project already exposes a container type, reuse it to keep the surface consistent.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getHostForService(service: any): string {
if (!service || !service.networkSettings?.Networks) return baseServerUrl;
const networks = service.networkSettings.Networks;
for (const networkName in networks) {
const network = networks[networkName];
if (network.Driver === 'macvlan' || network.Driver === 'ipvlan') {
if (network.IPAddress) return network.IPAddress;
}
}
return baseServerUrl;
}
import type { ContainerInfo } from '$lib/types/docker'; // or whatever you export
function getHostForService(service: Partial<ContainerInfo>): string {
if (!service || !service.networkSettings?.Networks) return baseServerUrl;
const networks = service.networkSettings.Networks;
for (const networkName in networks) {
const network = networks[networkName];
if (network.Driver === 'macvlan' || network.Driver === 'ipvlan') {
if (network.IPAddress) return network.IPAddress;
}
}
return baseServerUrl;
}
🧰 Tools
🪛 GitHub Check: ESLint

[failure] 83-83: Disallow the any type
Unexpected any. Specify a different type.

🤖 Prompt for AI Agents (early access)
In src/routes/stacks/[stackId]/+page.svelte around lines 83 to 96, the function
getHostForService currently types its parameter as any, causing ESLint no-any
violations. Replace the any type with a more specific interface that at least
includes the optional networkSettings property, or use
Partial<ContainerInspectInfo> if available. If the project already defines a
container type, reuse that type for consistency. This change will narrow the
parameter type and eliminate the ESLint warnings.

PublicPort?: number;
PrivatePort?: number;
Type?: string;
[key: string]: any;

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
@kmendell kmendell merged commit 420cf91 into main May 8, 2025
6 of 7 checks passed
@kmendell kmendell deleted the feat/service-link branch May 8, 2025 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚡️ Feature: Add Service Link with Configurable Service Address

1 participant