diff --git a/db/migrations/20260310094104_initial.sql b/db/migrations/20260310094104_initial.sql index 6c8afc4..22544a5 100644 --- a/db/migrations/20260310094104_initial.sql +++ b/db/migrations/20260310094104_initial.sql @@ -171,7 +171,7 @@ CREATE TABLE audit_logs ( metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX idx_audit_logs_team_time ON audit_logs(team_id, created_at DESC); +CREATE INDEX idx_audit_logs_team_time ON audit_logs(team_id, created_at DESC, id DESC); CREATE INDEX idx_audit_logs_team_resource ON audit_logs(team_id, resource_type, created_at DESC); -- sandbox_metrics_snapshots 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/db/queries/teams.sql b/db/queries/teams.sql index 3444b8c..f4de808 100644 --- a/db/queries/teams.sql +++ b/db/queries/teams.sql @@ -86,6 +86,14 @@ WHERE ut.user_id = $1 WHERE ut2.team_id = t.id AND ut2.user_id <> $1 ); +-- name: GetOwnedTeamIDs :many +-- Returns team IDs where the given user has the 'owner' role. +SELECT t.id FROM teams t +JOIN users_teams ut ON ut.team_id = t.id +WHERE ut.user_id = $1 + AND ut.role = 'owner' + AND t.deleted_at IS NULL; + -- name: CountTeamsAdmin :one SELECT COUNT(*)::int AS total FROM teams diff --git a/db/queries/users.sql b/db/queries/users.sql index 1c902a3..eb41d00 100644 --- a/db/queries/users.sql +++ b/db/queries/users.sql @@ -4,10 +4,10 @@ VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetUserByEmail :one -SELECT * FROM users WHERE email = $1; +SELECT * FROM users WHERE email = $1 AND status != 'deleted'; -- name: GetUserByID :one -SELECT * FROM users WHERE id = $1; +SELECT * FROM users WHERE id = $1 AND status != 'deleted'; -- name: InsertUserOAuth :one INSERT INTO users (id, email, name) @@ -63,14 +63,14 @@ SELECT (SELECT COUNT(*) FROM users_teams ut WHERE ut.user_id = u.id)::int AS teams_joined, (SELECT COUNT(*) FROM users_teams ut WHERE ut.user_id = u.id AND ut.role = 'owner')::int AS teams_owned FROM users u -WHERE u.deleted_at IS NULL +WHERE u.status != 'deleted' ORDER BY u.created_at DESC LIMIT $1 OFFSET $2; -- name: CountUsersAdmin :one SELECT COUNT(*)::int AS total FROM users -WHERE deleted_at IS NULL; +WHERE status != 'deleted'; -- name: SetUserStatus :exec UPDATE users SET status = $2, updated_at = NOW() WHERE id = $1; diff --git a/envd/internal/services/process/handler/multiplex.go b/envd/internal/services/process/handler/multiplex.go index a3f6ea3..88f0916 100644 --- a/envd/internal/services/process/handler/multiplex.go +++ b/envd/internal/services/process/handler/multiplex.go @@ -35,27 +35,27 @@ func NewMultiplexedChannel[T any](buffer int) *MultiplexedChannel[T] { c.mu.RUnlock() } + c.mu.Lock() c.exited.Store(true) - for _, cons := range c.channels { close(cons) } + c.mu.Unlock() }() return c } func (m *MultiplexedChannel[T]) Fork() (chan T, func()) { + m.mu.Lock() + defer m.mu.Unlock() + if m.exited.Load() { ch := make(chan T) close(ch) - return ch, func() {} } - m.mu.Lock() - defer m.mu.Unlock() - consumer := make(chan T, 4096) m.channels = append(m.channels, consumer) diff --git a/frontend/src/app.css b/frontend/src/app.css index 7cad308..d76358b 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -180,6 +180,13 @@ body { 50% { transform: translateY(-6px); } } +/* CSS containment — isolate paint for independent UI regions. + Note: `contain: layout` is omitted because it creates a containing block + that breaks `position: fixed` popups rendered inside
. */ +main { + contain: style; +} + /* Respect user motion preferences — covers both CSS class animations and inline style animations */ @media (prefers-reduced-motion: reduce) { *, diff --git a/frontend/src/lib/components/AuthModal.svelte b/frontend/src/lib/components/AuthModal.svelte deleted file mode 100644 index fddedaa..0000000 --- a/frontend/src/lib/components/AuthModal.svelte +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - - - - -
- -
- - {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}
- + + + @@ -849,6 +848,7 @@ {createForm.archive.name} + {:else} + + {team.name} + + {/if} {/if} @@ -717,6 +708,7 @@