Skip to content

feat(containers): compose editor + compose tab improvements#2052

Merged
kmendell merged 29 commits intogetarcaneapp:mainfrom
mkaltner:feat/container-compose-editor
Mar 21, 2026
Merged

feat(containers): compose editor + compose tab improvements#2052
kmendell merged 29 commits intogetarcaneapp:mainfrom
mkaltner:feat/container-compose-editor

Conversation

@mkaltner
Copy link
Contributor

@mkaltner mkaltner commented Mar 13, 2026

Summary

Adds a Compose tab to the individual container detail view. When a container belongs to an Arcane-managed project, the tab loads the relevant compose file — whether defined directly in the root compose or in an included sub-file — and allows viewing and editing it without leaving the container view.

Fixes: #1554


Changes

+page.ts

Loads the associated Arcane project during page load by matching the container's com.docker.compose.project label against project names via getProjectsForEnvironment (search by name, then fetch full detail by ID). Returns project as part of page data.

+page.svelte

  • Derives composeServiceName from the com.docker.compose.service container label
  • Computes serviceComposeSource: scans root compose first, then project.includeFiles[], to find which file directly defines services.{serviceName}
    • Returns { includeFile: null } when the service is in the root compose
    • Returns { includeFile: <file> } when the service is in an included sub-file
    • Returns null (tab hidden) when the service is not found anywhere
  • Adds a Compose tab when serviceComposeSource is non-null
  • Renders ContainerComposePanel with the project and resolved include file

ContainerComposePanel.svelte (new)

  • Accepts project, serviceName, and optional includeFile props
  • Displays the correct file: includeFile.content for sub-files, project.composeContent for root
  • Info banner shows the actual filename and links back to the project page
  • Saves via the right endpoint: updateProjectIncludeFile for sub-files, updateProject for root
  • Shows a read-only alert for GitOps-managed projects
  • fileId scoped per file so editor state does not collide across services

Behavior

  • Service in root compose.yml — tab shown, editable, saves via updateProject
  • Service in an include sub-file — tab shown, correct sub-file loaded (e.g. ollama/compose.yml), saves via updateProjectIncludeFile
  • Service not found in any file — tab hidden
  • GitOps managed — tab shown, read-only, alert displayed

PR Checklist

  • Code builds successfully in development environment (./scripts/development/dev.sh start)
  • Frontend loads at http://localhost:3000 with hot reload working
  • Backend responds at http://localhost:3552 with hot reload working
  • No linting errors: just lint frontend (currently 0 errors, 8 pre-existing CSS warnings unrelated to this PR)
  • Formatting passes: just format frontend
  • Commit messages follow conventional format ✅
  • PR description explains the change and why it is needed ✅

Manual Testing

  • Compose tab appears and loads the correct sub-file for a container in an include:-based project (e.g. ai-ollama-1ollama/compose.yml)
  • Compose tab is hidden for containers not associated with any Arcane project
  • Compose tab is hidden when the service is not defined in any known compose file
  • Save works correctly for root compose files
  • Save works correctly for include sub-files
  • GitOps-managed projects show read-only alert and disable the save button

AI Disclosure

AI Tool: Claude Code (Anthropic)
Assistance Level: Significant — implementation was AI-generated with human direction, review, and testing oversight per AI_POLICY.md

Disclaimer Greptiles Reviews use AI, make sure to check over its work.

To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike

To have Greptile Re-Review the changes, mention greptileai.

Greptile Summary

This PR adds a Compose tab to the container detail view, allowing users to view and edit the relevant compose file (root or include sub-file) directly from the container page. The implementation correctly extracts Docker Compose labels into a ComposeInfo struct on the backend, looks up the matching Arcane project during page load, performs YAML service-name lookups to find which file owns the service, and uses {#key} remounting to safely handle container/file switches.

Previous review issues have been thoroughly addressed — isDirty is correctly $derived, the {#key} now includes project.id, the save button is disabled when content is unchanged, invalidateAll() is called after save, the info banner conditionally says "Viewing"/"Editing", and all {@html} usage has been eliminated.

Remaining issues:

  • In ContainerComposePanel.svelte, toast.success is called after await invalidateAll(). If invalidateAll() rejects (transient network error), the catch block shows an error toast even though the server-side save already succeeded, misleading the user into retrying a no-op.
  • fileTitle hardcodes 'compose.yml' for root compose files. The actual filename (available from composeInfo.configFiles) could be docker-compose.yml, docker-compose.yaml, etc. The banner text would show an inaccurate filename in those cases.

Confidence Score: 4/5

  • Safe to merge — the feature works correctly for all documented scenarios and previous critical issues have been resolved.
  • All high-severity issues from prior review rounds (dirty tracking, stale content on container switch, missing project.id in key, disabled save button, XSS via @html, pagination miss) have been addressed. The two remaining issues are minor: a success-toast ordering edge case and a cosmetic filename label inaccuracy. Neither causes data loss or incorrect behaviour in the happy path.
  • frontend/src/routes/(app)/containers/components/ContainerComposePanel.svelte — toast ordering and hardcoded filename title.

Important Files Changed

Filename Overview
frontend/src/routes/(app)/containers/components/ContainerComposePanel.svelte New component for viewing/editing compose files from the container detail view. Previous issues (dirty-tracking, XSS, read-only label, save button state, key-based remount) are all resolved. Minor remaining issues: success toast fires after invalidateAll() (could mislead on navigation error), and fileTitle hardcodes 'compose.yml' for root compose.
frontend/src/routes/(app)/containers/[containerId]/+page.svelte Adds compose tab with correct {#key} using project.id + include path, YAML service lookup via $derived IIFE, and conditional tab visibility. The key expression is now correct. The YAML parse runs on every reactive update but compose files are small enough that this is acceptable.
frontend/src/routes/(app)/containers/[containerId]/+page.ts Project lookup now uses queryClient with explicit pagination limit (100). Error handling correctly uses err instanceof Error, error re-thrown for 404. Sequential waterfall for project lookup (2 extra RTTs) is a known trade-off documented in previous threads.
types/container/container.go ComposeInfo struct cleanly extracts projectName, serviceName, workingDir, and configFiles from Docker Compose labels. All standard labels used correctly. Previously flagged ConfigFile/ProjectDir label mismatches appear to have been removed in this version.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Container Page Load] --> B{composeInfo present?}
    B -->|No| C[No Compose Tab]
    B -->|Yes| D["Search projects by name\n(limit: 100)"]
    D --> E{Exact name match?}
    E -->|No| C
    E -->|Yes| F[Fetch full project detail]
    F --> G[Parse root composeContent YAML]
    G --> H{Service defined in root?}
    H -->|Yes| I["serviceComposeSource =\n{ includeFile: null }"]
    H -->|No| J[Iterate includeFiles]
    J --> K{Service in include file?}
    K -->|Yes| L["serviceComposeSource =\n{ includeFile: f }"]
    K -->|No - exhausted| C
    I --> M["Compose Tab shown\n{#key project.id + 'root'}"]
    L --> N["Compose Tab shown\n{#key project.id + relativePath}"]
    M --> O{GitOps managed?}
    N --> O
    O -->|Yes| P[Read-only editor\nViewing banner]
    O -->|No| Q[Editable editor\nEditing banner]
    Q --> R[User edits → isDirty=true\nSave button enabled]
    R --> S[handleSave]
    S --> T{includeFile?}
    T -->|Yes| U[updateProjectIncludeFile]
    T -->|No| V[updateProject]
    U --> W[invalidateAll → isDirty=false]
    V --> W
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: frontend/src/routes/(app)/containers/components/ContainerComposePanel.svelte
Line: 41-42

Comment:
**Success toast gated on `invalidateAll()` succeeding**

`toast.success` is called on line 42, but only _after_ `await invalidateAll()` on line 41. If `invalidateAll()` rejects (e.g., a transient network hiccup), the catch block fires and shows an error toast — even though the compose file was already persisted on the server. The user is told the save failed when it actually succeeded.

The fix is to toast before the invalidation (which is just a UI refresh, not part of the write path):

```suggestion
			await invalidateAll();
			toast.success(m.container_compose_save_success());
```

```ts
toast.success(m.container_compose_save_success());
await invalidateAll();
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: frontend/src/routes/(app)/containers/components/ContainerComposePanel.svelte
Line: 31

Comment:
**`fileTitle` hardcodes `'compose.yml'` for root compose**

When `includeFile` is `null`, `fileTitle` is always `'compose.yml'`, which appears verbatim in the info banner ("Editing **compose.yml** for project…"). The actual root compose file could legitimately be named `docker-compose.yml`, `docker-compose.yaml`, or `compose.yaml`. Docker Compose v2 accepts all four variants, and the actual filename is already available in `composeInfo.configFiles` (the `com.docker.compose.project.config_files` label value surfaced via the container's `ComposeInfo`).

While `composeInfo` is not a prop on this component, it's accessible in the parent `+page.svelte`. Consider passing the filename hint as an optional prop (or deriving a display name in the parent from `composeInfo.configFiles`) so the banner reflects the real filename rather than always defaulting to `'compose.yml'`.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: c844b33

Greptile also left 2 inline comments on this PR.

mkaltner and others added 6 commits March 13, 2026 18:13
…iners (getarcaneapp#1554)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ainerComposePanel component

- Fix project lookup: search by name via getProjectsForEnvironment then fetch by ID,
  instead of incorrectly passing project name to getProject (which expects an ID)
- Move project loading to +page.ts data loader (proper SvelteKit pattern)
- Extract inline CodePanel usage into dedicated ContainerComposePanel.svelte component
- Support editable compose file with save via projectService.updateProject
- Show read-only alert for GitOps-managed projects
- Use untrack() on initial state to satisfy Svelte 5 reactive reference rules
- Link back to project page via ExternalLink button

Closes getarcaneapp#1554
…-compose

When a project's compose.yml uses `include:` directives (a meta/wrapper compose),
the service definition lives in a sub-file — not the root compose. Showing and editing
the root compose from a container view is misleading and not useful.

Parse the project's composeContent with the yaml library to check whether
`services.{serviceName}` exists directly in the top-level services block.
If not, suppress the Compose tab entirely for that container.

Also tightens the info banner copy to drop the redundant
'Changes to this file affect all services' sentence (now always accurate since
we only show direct-service composes).
…ived not a stored function

$derived((): boolean => {...}) stores a function as the derived value; Svelte 5
doesn't track reactive reads (project, composeServiceName) when the stored function
is called later from inside tabItems. Use an IIFE inside $derived instead so the
boolean is computed directly and reactivity is properly tracked.
…including sub-files)

When a container belongs to a project with `include:` directives, find the
include file that directly defines the service and show/edit that instead of
the root wrapper compose.

- serviceComposeSource derived: scans root compose, then includeFiles[], to find
  which file has `services.{serviceName}` defined
- Shows tab for both root-defined and include-file-defined services
- Hides tab only when the service isn't found in any file
- ContainerComposePanel now accepts optional `includeFile` prop
  - Displays includeFile.content instead of project.composeContent
  - Saves via projectService.updateProjectIncludeFile (relativePath + content)
  - Falls back to updateProject for root compose
  - fileTitle shows the actual file name (e.g. ollama/compose.yml)
  - fileId scoped per include file so editor state doesn't collide
@mkaltner mkaltner requested a review from a team March 13, 2026 23:01
@mkaltner
Copy link
Contributor Author

I'm not going to blindly approve those AI suggestions like last time.
I've tested this locally.
Feel free to review and comment as necessary

@kmendell
Copy link
Member

I would prefer to do most of this in the backend , vs all of it client side in the browser, that will just increase load time , go tends to be faster for loading this stuff.

If the label exists just then incldue it in the reponse. Then if that is included the tab would show conditionally.

As far as greptiles comments, they are all valid state being updated in an effect is aggainst sveltes reomendations. And there are a few missing i18n strings.

@mkaltner
Copy link
Contributor Author

Unfortunately, I don't think there's a good way to determine the necessary information in backend. I'll investigate further.

@github-actions
Copy link

This pull request has merge conflicts. Please resolve the conflicts so the PR can stay up-to-date and reviewed.

- Add ComposeInfo to backend container Details response with Docker Compose metadata
- Extract compose project/service info from labels in Go (backend-side processing)
- Add i18n strings for all hardcoded text in ContainerComposePanel component
- Simplify frontend logic to use composeInfo from API instead of label parsing
- Fix Svelte reactivity pattern to properly track source content changes
- Remove redundant Svelte effect antipattern per code review feedback

Addresses code review feedback to move heavy lifting to backend and ensure
proper internationalization coverage.
@mkaltner mkaltner force-pushed the feat/container-compose-editor branch from c19f751 to 678f8f2 Compare March 14, 2026 12:32
Resolved conflict in types/container/container.go by keeping both:
- ComposeInfo struct (from feature branch)
- SummaryGroup struct (from upstream main)

Both structs serve different purposes and can coexist without conflict.
@kmendell
Copy link
Member

@mkaltner You should be able to inspect the labels of teh container, i do that in other places

Michael Kaltner and others added 2 commits March 14, 2026 16:57
1. Add dirty tracking to prevent unsaved edits from being lost on data refresh
   - Track isDirty state based on composeContent vs sourceContent
   - Only update editor when source changes AND no unsaved edits exist
   - Reset isDirty flag after successful save

2. Add 'Viewing' vs 'Editing' label for read-only mode
   - Added container_compose_viewing_info i18n string
   - Conditionally display 'Viewing' when isReadOnly is true
   - Maintains consistent i18n coverage

3. Remove unused saveError state variable
   - Errors are already surfaced via toast notifications
   - Simplifies component state management

4. Add explicit pagination limit to project lookup
   - Pass pagination: { page: 1, limit: 100 } to getProjectsForEnvironment
   - Ensures compose tab appears even with many projects (>20)
   - Prevents silently missing projects beyond default page size

Addresses automated review feedback from greptile-apps[bot].
@mkaltner
Copy link
Contributor Author

Please see the latest push, this should address your concerns as well as the code review comments.

Thanks!

@mkaltner
Copy link
Contributor Author

🔧 Fix for gocognit linter failure

The CI is failing because NewDetails has cognitive complexity 32 (limit is 30). Here's a refactor that brings it under the threshold by extracting helper functions:

Changes

  • Extract extractPorts() for port binding logic
  • Extract extractComposeInfo() for Docker Compose label parsing
  • No functional changes — pure complexity reduction

Patch

diff --git a/types/container/container.go b/types/container/container.go
index 7890d9ce..6d54203b 100644
--- a/types/container/container.go
+++ b/types/container/container.go
@@ -832,33 +832,69 @@ func NewSummary(c container.Summary) Summary {
 }
 
 // NewDetails creates a Details from a docker container.InspectResponse.
-func NewDetails(c *container.InspectResponse) Details {
+func extractPorts(c *container.InspectResponse) []Port {
 	ports := make([]Port, 0)
-	if c.NetworkSettings != nil && c.NetworkSettings.Ports != nil {
-		for p, bindings := range c.NetworkSettings.Ports {
-			privatePort := int(p.Num())
-			typ := string(p.Proto())
-
-			// When no host bindings exist, still include the private port
-			if len(bindings) == 0 {
-				ports = append(ports, Port{
-					PrivatePort: privatePort,
-					Type:        typ,
-				})
-				continue
-			}
-			for _, b := range bindings {
-				pub, _ := strconv.Atoi(b.HostPort)
-				ports = append(ports, Port{
-					IP:          b.HostIP.String(),
-					PrivatePort: privatePort,
-					PublicPort:  pub,
-					Type:        typ,
-				})
-			}
+	if c.NetworkSettings == nil || c.NetworkSettings.Ports == nil {
+		return ports
+	}
+
+	for p, bindings := range c.NetworkSettings.Ports {
+		privatePort := int(p.Num())
+		typ := string(p.Proto())
+
+		// When no host bindings exist, still include the private port
+		if len(bindings) == 0 {
+			ports = append(ports, Port{
+				PrivatePort: privatePort,
+				Type:        typ,
+			})
+			continue
 		}
+		for _, b := range bindings {
+			pub, _ := strconv.Atoi(b.HostPort)
+			ports = append(ports, Port{
+				IP:          b.HostIP.String(),
+				PrivatePort: privatePort,
+				PublicPort:  pub,
+				Type:        typ,
+			})
+		}
+	}
+	return ports
+}
+
+func extractComposeInfo(labels map[string]string) *ComposeInfo {
+	projectName, hasProject := labels["com.docker.compose.project"]
+	if !hasProject {
+		return nil
+	}
+
+	serviceName, hasService := labels["com.docker.compose.service"]
+	if !hasService {
+		return nil
+	}
+
+	info := &ComposeInfo{
+		ProjectName: projectName,
+		ServiceName: serviceName,
+	}
+
+	if configFile, ok := labels["com.docker.compose.config-files"]; ok {
+		info.ConfigFile = configFile
+	}
+	if workingDir, ok := labels["com.docker.compose.project.working_dir"]; ok {
+		info.WorkingDir = workingDir
+	}
+	if projectDir, ok := labels["com.docker.compose.project.config_files"]; ok {
+		info.ProjectDir = projectDir
+	}
+
+	return info
+}
+
+func NewDetails(c *container.InspectResponse) Details {
+	ports := extractPorts(c)
+
 	mounts := make([]Mount, 0, len(c.Mounts))
 	for _, m := range c.Mounts {
 		mounts = append(mounts, Mount{
@@ -922,24 +958,7 @@ func NewDetails(c *container.InspectResponse) Details {
 	}
 
 	// Extract Docker Compose information from labels if present
-	var composeInfo *ComposeInfo
-	if projectName, hasProject := labels["com.docker.compose.project"]; hasProject {
-		if serviceName, hasService := labels["com.docker.compose.service"]; hasService {
-			composeInfo = &ComposeInfo{
-				ProjectName: projectName,
-				ServiceName: serviceName,
-			}
-			if configFile, ok := labels["com.docker.compose.config-files"]; ok {
-				composeInfo.ConfigFile = configFile
-			}
-			if workingDir, ok := labels["com.docker.compose.project.working_dir"]; ok {
-				composeInfo.WorkingDir = workingDir
-			}
-			if projectDir, ok := labels["com.docker.compose.project.config_files"]; ok {
-				composeInfo.ProjectDir = projectDir
-			}
-		}
-	}
+	composeInfo := extractComposeInfo(labels)
 
 	return Details{
 		ID:         c.ID,

To apply:

cd ~/Projects/arcane
git checkout feat/container-compose-editor
curl -o /tmp/fix.patch https://gist.github.com/... # or copy the diff above
git apply /tmp/fix.patch
git add types/container/container.go
git commit -m "refactor(types): reduce NewDetails cognitive complexity"
git push

Verified: builds successfully with go build ./...

@mkaltner
Copy link
Contributor Author

@greptileai Please re-review — all four issues from the previous review were addressed in commit 1d802f1.

Michael Kaltner and others added 2 commits March 14, 2026 17:58
…er-controlled values

- Replace $effect-based isDirty state with $derived for proper Svelte 5 reactivity
- Remove manual isDirty reset in handleSave (now auto-computed)
- Add escapeHtml() utility to prevent XSS vulnerabilities
- Escape all user-controlled values (project name, file paths, service names) before {@html} interpolation

Addresses Greptile security and code quality feedback.
- Fix XSS vulnerability by decomposing info banner to native Svelte (no {@html})
- Change isDirty to $derived instead of $state in ContainerComposePanel (Svelte rule violation)
- Remove dead isDirty = false reset in handleSave
- Rename ProjectDir to ConfigFiles and update doc comment for clarity
- Add tabs_compose i18n key and use it in Compose tab label
@mkaltner
Copy link
Contributor Author

@greptileai All four issues addressed in 74864d8:

  1. XSS vulnerability fixed - replaced {@html} with native Svelte elements
  2. isDirty already from previous commit
  3. Backend ProjectDir renamed to ConfigFiles for clarity
  4. tabs_compose i18n key added

Please re-review.

@mkaltner
Copy link
Contributor Author

Re: Greptile feedback

All issues addressed in 74864d8:

XSS vulnerability (ContainerComposePanel.svelte) - Replaced {@html} with native Svelte elements. Info banner now uses <strong>, <a>, and plain text - no HTML interpolation.

$state in $effect (ContainerComposePanel.svelte) - isDirty was already converted to $derived in commit 1d802f1, and the dead isDirty = false reset was already removed. No further changes needed.

Backend field mismatch (container.go) - Renamed ProjectDirConfigFiles with updated doc comment. Also updated TypeScript type to match.

Hardcoded i18n (+page.svelte) - Added tabs_compose key to en.json and updated tab label to use m.tabs_compose().

Thank you for the thorough security review!

- Remove {@html} interpolation with user-controlled values
  (project.name, serviceName, includeFile.relativePath)
  Plain text i18n messages don't need {@html}, prevents XSS
- Wrap $state mutations in untrack() to avoid reactivity cycle
  Reads reactive sources outside, writes inside untrack()
  Fixes Svelte 5 anti-pattern per codebase rules

Addresses core Greptile review concerns for PR getarcaneapp#2052
@mkaltner
Copy link
Contributor Author

Core security and reactivity issues fixed in commit 2521a72

Addressed the 3/5 confidence score blockers:

  1. XSS via {@html} with user-controlled values (line 86)

    • Removed {@html} interpolation
    • i18n messages (container_compose_viewing_info / container_compose_editing_info) are plain text with placeholders, not HTML
    • Now using safe Svelte text interpolation — no XSS vector
  2. $state mutations inside $effect blocks (lines 43-51, Svelte 5 anti-pattern)

    • Wrapped all $state writes in untrack() to break reactivity cycle
    • Reads reactive sources (fileIdentity, sourceContent) outside untrack — these trigger the effect
    • Writes (composeContent, prevSourceContent, prevFileIdentity) inside untrack — these don't re-trigger
    • Follows codebase rule: "Avoid updating $state inside $effect blocks"

Previous commit (63bd939) fixed the two new findings:

  • Invalid fileId with slashes in paths
  • YAML parsing performance (memoization)

@greptileai please re-review — all flagged issues should now be resolved

- Use {#key} remount strategy instead of $state mutations in $effect
  Parent wraps ContainerComposePanel with {#key} on file path,
  forcing clean remount on context switch (no manual tracking)
- Remove Map cache side effect from $derived block
  YAML files are small, re-parsing is acceptable
- Change catch (err: any) to catch (err: unknown) with proper narrowing

All three Greptile-flagged issues resolved per project rules
@mkaltner
Copy link
Contributor Author

All Svelte 5 reactivity rule violations resolved in commit 78ddf74

Changes:

  1. $state mutations in $effect → Eliminated

    • Used {#key} remount strategy in parent page
    • ContainerComposePanel now initializes cleanly, no effect/prev-tracking needed
    • Removed 32 lines of complexity
  2. Map.set() side effect in $derived → Removed

    • Deleted the cache entirely (YAML files are small, re-parsing is fine)
    • hasService() is now a pure function
  3. catch (err: any)catch (err: unknown)

    • Proper type narrowing with instanceof Error

All three Greptile-flagged issues properly fixed per project rules.

@greptileai please re-review

Two containers from different projects both using root compose
would share the same key ('root'), causing stale content and
potential cross-project save (data loss).

Key now includes project ID: `${project?.id}-${relativePath ?? 'root'}`
@mkaltner
Copy link
Contributor Author

Fixed cross-project key collision in commit ba62a3d

Changed {#key} from:

{#key serviceComposeSource?.includeFile?.relativePath ?? 'root'}

To:

{#key \`${project?.id}-${serviceComposeSource?.includeFile?.relativePath ?? 'root'}\`}

Now two containers from different projects won't share the same key even if both use root compose.

Frontend build verified ✅

@greptileai please re-review

mkaltner and others added 2 commits March 14, 2026 15:29
- Add disabled={!isDirty} to save button to prevent unnecessary API calls
- Route project list fetch through queryClient for cache benefits on repeat nav
@mkaltner
Copy link
Contributor Author

Minor UX/perf fixes in commit c844b33

  1. Save button disabled when !isDirty — no more wasted API calls when nothing changed
  2. Project list fetch through queryClient — cache benefits on repeat navigation

Build verified ✅

@greptileai please re-review

mkaltner and others added 3 commits March 16, 2026 09:55
- Move success toast before invalidateAll() to avoid misleading error on cache refresh failure
- Pass actual root compose filename instead of hardcoding compose.yml
@mkaltner
Copy link
Contributor Author

Seems like a timeout error in the build. I think everything else is good now.

@kmendell
Copy link
Member

The more i think about this the more im aggainst it. Id be fine with a lint in the tab bar to take you to the proejct but adding a editor here, just seems redundant honestly. and duplciated code. IF that makes sense, a link to eidt the project would make more sense to me.

@mkaltner
Copy link
Contributor Author

mkaltner commented Mar 18, 2026

Well, not everyone uses a single compose per stack/project. I certainly don't. Others may agree. A link to take me to the project is unnecessary IMHO. When I'm working with the container in question, troubleshooting it I'd love to be able o directly modify my compose and redeploy from the same screen. I guess I have and advanced scenario but that doesn't means it's not legitimate. I don't think it hurts to add but at the end of the day, it's your project.

My project compose looks like this:
image

Then I have to find my compose:
image

Then to redeploy I have to redeploy the whole project (that's fixed with my other PR).

It would save people in my situation a lot of time to have a compose and redeploy right in the container screen.

Extract duplicated compose editor UI (GitOps alert, info banner, save
button, view project link, toast+invalidateAll logic) into a reusable
ComposeEditorWrapper component in $lib/components/compose/. Update
ContainerComposePanel to delegate to it via an onSave callback.

Co-authored-by: Michael Kaltner <mkaltner@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@mkaltner
Copy link
Contributor Author

I pushed a small refactor. ContainerComposePanel now uses a shared ComposeEditorWrapper component. The editor itself (CodePanel) was already shared; this just consolidates the surrounding UI chrome too.

On the broader point: I don't see this as duplicated functionality. It's the same functionality accessible from the appropriate context, like how you can restart a container from the container view or the project view, as well as the recently merged redeploy. Multiple entry points, same underlying action.

When I'm troubleshooting a specific container, I want to edit its config and redeploy without navigating away to find which project/file it lives in. That's the workflow this enables.

Happy to discuss further if you want, but I think this is a UX improvement worth having.

@kmendell
Copy link
Member

Okay i guess sure i get it. Ill look again tonight.

@kmendell
Copy link
Member

Lgtm, Thanks

@kmendell kmendell enabled auto-merge (squash) March 21, 2026 01:05
@kmendell kmendell merged commit 70ddfd8 into getarcaneapp:main Mar 21, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚡️ Feature: Add the ability to modify a compose file from the container view

2 participants