From e91109d69c4d6b16195a494da80720243e8982e0 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Thu, 16 Apr 2026 05:29:02 +0600 Subject: [PATCH 1/4] Fix API key cleanup on user deactivation and build archive race condition Delete all API keys created by a user when their account is disabled, deleted, or soft-deleted. Store build archives before enqueuing to Redis so workers never dequeue a build with missing files. --- db/queries/api_keys.sql | 3 +++ internal/api/handlers_me.go | 4 ++++ pkg/db/api_keys.sql.go | 9 +++++++++ pkg/service/build.go | 12 ++++++------ pkg/service/user.go | 6 ++++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/db/queries/api_keys.sql b/db/queries/api_keys.sql index be064a1..f57bf5b 100644 --- a/db/queries/api_keys.sql +++ b/db/queries/api_keys.sql @@ -25,3 +25,6 @@ UPDATE team_api_keys SET last_used = NOW() WHERE id = $1; -- name: DeleteAPIKeysByTeam :exec DELETE FROM team_api_keys WHERE team_id = $1; + +-- name: DeleteAPIKeysByCreator :exec +DELETE FROM team_api_keys WHERE created_by = $1; diff --git a/internal/api/handlers_me.go b/internal/api/handlers_me.go index aefb7d7..c70e302 100644 --- a/internal/api/handlers_me.go +++ b/internal/api/handlers_me.go @@ -524,6 +524,10 @@ func (h *meHandler) DeleteAccount(w http.ResponseWriter, r *http.Request) { } } + if err := h.db.DeleteAPIKeysByCreator(ctx, ac.UserID); err != nil { + slog.Warn("account delete: failed to delete user's API keys", "error", err) + } + if err := h.db.SoftDeleteUser(ctx, ac.UserID); err != nil { writeError(w, http.StatusInternalServerError, "db_error", "failed to delete account") return diff --git a/pkg/db/api_keys.sql.go b/pkg/db/api_keys.sql.go index 55f1bce..b157931 100644 --- a/pkg/db/api_keys.sql.go +++ b/pkg/db/api_keys.sql.go @@ -25,6 +25,15 @@ func (q *Queries) DeleteAPIKey(ctx context.Context, arg DeleteAPIKeyParams) erro return err } +const deleteAPIKeysByCreator = `-- name: DeleteAPIKeysByCreator :exec +DELETE FROM team_api_keys WHERE created_by = $1 +` + +func (q *Queries) DeleteAPIKeysByCreator(ctx context.Context, createdBy pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteAPIKeysByCreator, createdBy) + return err +} + const deleteAPIKeysByTeam = `-- name: DeleteAPIKeysByTeam :exec DELETE FROM team_api_keys WHERE team_id = $1 ` diff --git a/pkg/service/build.go b/pkg/service/build.go index 70e67f7..bdb1620 100644 --- a/pkg/service/build.go +++ b/pkg/service/build.go @@ -139,16 +139,16 @@ func (s *BuildService) Create(ctx context.Context, p BuildCreateParams) (db.Temp return db.TemplateBuild{}, fmt.Errorf("insert build: %w", err) } - // Enqueue build ID (as formatted string) to Redis for workers to pick up. - if err := s.Redis.RPush(ctx, buildQueueKey, buildIDStr).Err(); err != nil { - return db.TemplateBuild{}, fmt.Errorf("enqueue build: %w", err) - } - - // Store archive for the worker if provided. + // Store archive before enqueue so the worker never dequeues without files. if len(p.Archive) > 0 { s.storeArchive(buildIDStr, p.Archive) } + if err := s.Redis.RPush(ctx, buildQueueKey, buildIDStr).Err(); err != nil { + s.takeArchive(buildIDStr) // clean up on enqueue failure + return db.TemplateBuild{}, fmt.Errorf("enqueue build: %w", err) + } + return build, nil } diff --git a/pkg/service/user.go b/pkg/service/user.go index db687c0..9a3a66c 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "log/slog" "time" "github.com/jackc/pgx/v5/pgtype" @@ -66,5 +67,10 @@ func (s *UserService) SetUserStatus(ctx context.Context, userID pgtype.UUID, sta }); err != nil { return fmt.Errorf("set user status: %w", err) } + if status == "disabled" || status == "deleted" { + if err := s.DB.DeleteAPIKeysByCreator(ctx, userID); err != nil { + slog.Warn("failed to delete API keys for deactivated user", "user_id", userID, "error", err) + } + } return nil } From ed2222c80ca5a257fdee7f1ecd99fc575c9771cb Mon Sep 17 00:00:00 2001 From: pptx704 Date: Thu, 16 Apr 2026 05:34:47 +0600 Subject: [PATCH 2/4] Move sidebar into layout files and fix timer cleanup across frontend Sidebar and AdminSidebar were re-instantiated on every page navigation (17 pages total), causing unnecessary DOM teardown/rebuild and redundant localStorage reads. Now each lives in its respective +layout.svelte as a single persistent instance. Also adds onDestroy cleanup for leaked timers (settings, team, login RAF loop) and CSS containment on
to isolate layout recalculations. --- frontend/src/app.css | 5 + frontend/src/routes/admin/+layout.svelte | 14 +- .../src/routes/admin/capsules/+page.svelte | 17 +- .../routes/admin/capsules/[id]/+page.svelte | 15 +- frontend/src/routes/admin/hosts/+page.svelte | 29 ++-- frontend/src/routes/admin/teams/+page.svelte | 17 +- .../src/routes/admin/templates/+page.svelte | 19 +-- frontend/src/routes/admin/users/+page.svelte | 19 +-- frontend/src/routes/dashboard/+layout.svelte | 14 +- .../src/routes/dashboard/audit/+page.svelte | 17 +- .../src/routes/dashboard/billing/+page.svelte | 19 +-- .../routes/dashboard/capsules/+layout.svelte | 145 ++++++++---------- .../routes/dashboard/channels/+page.svelte | 15 +- .../src/routes/dashboard/hosts/+page.svelte | 19 +-- .../src/routes/dashboard/keys/+page.svelte | 19 +-- .../src/routes/dashboard/metrics/+page.svelte | 15 +- .../routes/dashboard/settings/+page.svelte | 24 +-- .../src/routes/dashboard/team/+page.svelte | 23 +-- .../routes/dashboard/templates/+page.svelte | 15 +- .../src/routes/dashboard/usage/+page.svelte | 19 +-- frontend/src/routes/login/+page.svelte | 6 +- 21 files changed, 159 insertions(+), 326 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 7cad308..823f8ce 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -180,6 +180,11 @@ body { 50% { transform: translateY(-6px); } } +/* CSS containment — isolate layout/paint for independent UI regions */ +main { + contain: layout style; +} + /* Respect user motion preferences — covers both CSS class animations and inline style animations */ @media (prefers-reduced-motion: reduce) { *, diff --git a/frontend/src/routes/admin/+layout.svelte b/frontend/src/routes/admin/+layout.svelte index 599de61..de91ec9 100644 --- a/frontend/src/routes/admin/+layout.svelte +++ b/frontend/src/routes/admin/+layout.svelte @@ -1,7 +1,19 @@ +
+ +
+ {@render children()} +
+
-{@render children()} diff --git a/frontend/src/routes/admin/capsules/+page.svelte b/frontend/src/routes/admin/capsules/+page.svelte index 7c7e4b7..ccba40a 100644 --- a/frontend/src/routes/admin/capsules/+page.svelte +++ b/frontend/src/routes/admin/capsules/+page.svelte @@ -1,5 +1,4 @@ -
- +
+ +
+ +
-
- -
- -
- -
-
-

- Hosts +
+
+

+ Hosts

Platform and BYOC compute across all teams. @@ -303,7 +293,6 @@ {/if}

-
{#snippet skeletonRows()}
diff --git a/frontend/src/routes/admin/teams/+page.svelte b/frontend/src/routes/admin/teams/+page.svelte index bdf3a32..a09b3d9 100644 --- a/frontend/src/routes/admin/teams/+page.svelte +++ b/frontend/src/routes/admin/teams/+page.svelte @@ -1,5 +1,4 @@ -
- - -
- -
+
+ +
@@ -373,8 +363,7 @@ {/if} {/if}
-
- + diff --git a/frontend/src/routes/admin/users/+page.svelte b/frontend/src/routes/admin/users/+page.svelte index e933dc3..ca1960e 100644 --- a/frontend/src/routes/admin/users/+page.svelte +++ b/frontend/src/routes/admin/users/+page.svelte @@ -1,5 +1,4 @@ -{@render children()} +
+ +
+ {@render children()} +
+
diff --git a/frontend/src/routes/dashboard/audit/+page.svelte b/frontend/src/routes/dashboard/audit/+page.svelte index b4ee5d0..3539f5a 100644 --- a/frontend/src/routes/dashboard/audit/+page.svelte +++ b/frontend/src/routes/dashboard/audit/+page.svelte @@ -1,14 +1,7 @@ Wrenn — Capsules -
- - -
-
- - {#if $page.params.id} - -
-
- - Capsules - - - - - {$page.params.id} - - - -
-
- {:else} - -
-
-
-

- Capsules -

-

- All active and recent capsules across your team. -

-
- -
-
- - - - - {capsuleRunningCount.value} - running now -
-
-
- -
-
- {/if} - - {@render children()} -
- - -
-
- - - +
+ + {#if $page.params.id} + +
+
+ + Capsules + + + + + {$page.params.id} + + - All systems operational
-
+
+ {:else} + +
+
+
+

+ Capsules +

+

+ All active and recent capsules across your team. +

+
+ +
+
+ + + + + {capsuleRunningCount.value} + running now +
+
+
+ +
+
+ {/if} + + {@render children()} + + + +
+
+ + + + + All systems operational
-
+ diff --git a/frontend/src/routes/dashboard/channels/+page.svelte b/frontend/src/routes/dashboard/channels/+page.svelte index 0a57631..71c78ed 100644 --- a/frontend/src/routes/dashboard/channels/+page.svelte +++ b/frontend/src/routes/dashboard/channels/+page.svelte @@ -1,5 +1,4 @@ @@ -17,11 +10,7 @@ Wrenn — Metrics -
- - -
-
+

Metrics @@ -50,8 +39,6 @@ All systems operational

-
-
- import Sidebar from '$lib/components/Sidebar.svelte'; - import { onMount } from 'svelte'; + import { onMount, onDestroy } from 'svelte'; import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { auth } from '$lib/auth.svelte'; @@ -16,12 +15,6 @@ type MeResponse } from '$lib/api/me'; - let collapsed = $state( - typeof window !== 'undefined' - ? localStorage.getItem('wrenn_sidebar_collapsed') === 'true' - : false - ); - let me = $state(null); let loadError = $state(null); @@ -189,17 +182,18 @@ toast.error(connectErrors[connectErr] ?? 'Failed to connect account.'); } }); + + onDestroy(() => { + if (nameSavedTimer) clearTimeout(nameSavedTimer); + if (passwordSavedTimer) clearTimeout(passwordSavedTimer); + }); Wrenn — Settings -
- - -
-
+
@@ -504,9 +498,7 @@ {/if}
-
-
-
+
{#if showDisconnectConfirm} diff --git a/frontend/src/routes/dashboard/team/+page.svelte b/frontend/src/routes/dashboard/team/+page.svelte index 5470cfd..8146a16 100644 --- a/frontend/src/routes/dashboard/team/+page.svelte +++ b/frontend/src/routes/dashboard/team/+page.svelte @@ -1,6 +1,5 @@ @@ -318,11 +315,7 @@ }} /> -
- - -
-
+

@@ -811,9 +804,7 @@

-
-
-
+
{#if openDropdownId} diff --git a/frontend/src/routes/dashboard/templates/+page.svelte b/frontend/src/routes/dashboard/templates/+page.svelte index 550e9b5..f48d09e 100644 --- a/frontend/src/routes/dashboard/templates/+page.svelte +++ b/frontend/src/routes/dashboard/templates/+page.svelte @@ -1,5 +1,4 @@ - - - - - - - - - - -
- -
- - {title} - - - {subtitle} - -
- - - - - -
-
- or -
-
- - -
- {#if mode === 'signup'} -
-
- -
- -
- {/if} - -
-
- -
- -
- -
-
- -
- - -
- - {#if mode === 'signin'} -
- -
- {/if} - - -
- - -

- {switchText} - -

-
-
-
-
- - diff --git a/frontend/src/lib/components/FilesTab.svelte b/frontend/src/lib/components/FilesTab.svelte index bdc2a75..46c2d4a 100644 --- a/frontend/src/lib/components/FilesTab.svelte +++ b/frontend/src/lib/components/FilesTab.svelte @@ -81,7 +81,7 @@ ); // Breadcrumb segments from currentPath - const breadcrumbs = $derived(() => { + const breadcrumbs = $derived.by(() => { const parts = currentPath.split('/').filter(Boolean); const crumbs: { name: string; path: string }[] = [{ name: '/', path: '/' }]; for (let i = 0; i < parts.length; i++) { @@ -517,7 +517,7 @@ - {#each breadcrumbs() as crumb, i} + {#each breadcrumbs as crumb, i} {#if i > 0} @@ -526,7 +526,7 @@ {t.message}