diff --git a/frontend/src/lib/instanceRoutes.ts b/frontend/src/lib/instanceRoutes.ts
new file mode 100644
index 0000000000..7d57dc1b81
--- /dev/null
+++ b/frontend/src/lib/instanceRoutes.ts
@@ -0,0 +1,10 @@
+/**
+ * Pages a devops user can open without belonging to any workspace. Devops grants no workspace
+ * access, so such a page renders with `$workspaceStore` and `$userStore` unset and must hide
+ * whatever needs them (a workspace-scoped call or a link into a workspace) rather than break.
+ */
+const INSTANCE_ROUTES = ['/workers', '/service_logs']
+
+export function isInstanceRoute(pathname: string): boolean {
+ return INSTANCE_ROUTES.some((r) => pathname === r || pathname.startsWith(r + '/'))
+}
diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte
index 918b5cd4d7..3fe47a74fc 100644
--- a/frontend/src/routes/(root)/(logged)/+layout.svelte
+++ b/frontend/src/routes/(root)/(logged)/+layout.svelte
@@ -79,8 +79,11 @@
Home,
Play,
Search,
- WandSparkles
+ WandSparkles,
+ ArrowLeft
} from 'lucide-svelte'
+ import { Button } from '$lib/components/common'
+ import { isInstanceRoute } from '$lib/instanceRoutes'
import { getUserExt } from '$lib/user'
import { confirmPendingLoginMethod } from '$lib/lastLoginMethod'
import { deepEqual } from 'fast-equals'
@@ -796,6 +799,11 @@
}
let devOnly = $derived(page.url.pathname.startsWith('/scripts/dev'))
+ // A devops user who belongs to no workspace has no `$userStore`, and would otherwise wait on
+ // "Loading user..." forever on the instance pages their role exists for.
+ let instanceOnlyView = $derived(
+ !$workspaceStore && !!$devopsRole && isInstanceRoute(page.url.pathname)
+ )
async function loadDefaultScripts(workspace: string, user: UserExt | undefined) {
if (!user?.operator) {
@@ -1566,6 +1574,27 @@ outside this layout, and their shortcut must reopen the URL that is their creden
/>
+{:else if instanceOnlyView}
+
+
+
+
+
+
+
+
+
+ {@render children?.()}
+
+
{:else}
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte
index 2c5e626578..046f723e85 100644
--- a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte
@@ -9,6 +9,7 @@
import { UserService, type WorkspaceInvite, WorkspaceService } from '$lib/gen'
import {
superadmin,
+ devopsRole,
usersWorkspaceStore,
userWorkspaces,
workspaceStore,
@@ -30,7 +31,9 @@
Search,
ChevronsDownUp,
ChevronsUpDown,
- LogOut
+ LogOut,
+ Logs,
+ ServerCog
} from 'lucide-svelte'
import { isCloudHosted } from '$lib/cloud'
import { isValidLogoutRedirect, toSameOriginRelativePath } from '$lib/logoutRedirect'
@@ -169,17 +172,17 @@
let workspacesLoaded = $derived.by(
() => workspaces !== undefined && $usersWorkspaceStore !== undefined
)
- // Not for a superadmin: this page is also where they reach the instance settings and the
- // list-all toggle, and standing the picker down takes both away — a superadmin with no
- // membership of their own has business here besides creating a workspace. Waiting for the
- // store to answer rather than reading `!$superadmin`, which is true while `globalWhoami`
- // is still in flight.
+ // Not for a superadmin or devops user: this page is also where they reach the instance
+ // settings, the list-all toggle and the instance pages, and standing the picker down takes
+ // those away — they have business here besides creating a workspace. `$devopsRole` is truthy
+ // for a superadmin too. Waiting for the store to answer rather than reading `!$devopsRole`,
+ // which is true while `globalWhoami` is still in flight.
let nothingToChoose = $derived(
workspacesLoaded &&
invitesLoaded &&
createWorkspace &&
- $superadmin !== undefined &&
- !$superadmin &&
+ $devopsRole !== undefined &&
+ !$devopsRole &&
!list_all_as_super_admin &&
allWorkspaces.length === 0 &&
invites.length === 0
@@ -206,11 +209,11 @@
let showCreate = $state(false)
$effect(() => {
if (nothingToChoose) showCreate = true
- // Except for a superadmin, whom `nothingToChoose` excludes — so this only ever undoes a
- // latch that should not have happened: one taken on a stale `false` before the forced
- // `refreshSuperadmin` above answered. Without it that superadmin would be stuck on the
- // create form, instance settings and the list-all toggle gone with it, until a reload.
- else if ($superadmin) showCreate = false
+ // Except for a superadmin or devops user, whom `nothingToChoose` excludes — so this only
+ // ever undoes a latch that should not have happened: one taken on a stale `false` before
+ // the forced `refreshSuperadmin` above answered. Without it they would be stuck on the
+ // create form, with what they came here for gone with it, until a reload.
+ else if ($devopsRole) showCreate = false
})
async function speakFriendAndEnterWorkspace(workspaceId: string) {
@@ -518,6 +521,20 @@
{/if}
{/if}
+
+ {#if !showCreate && $devopsRole && !$superadmin}
+
Instance
+
+
+
+
+ {/if}
+
diff --git a/frontend/src/routes/(root)/(logged)/workers/+page.svelte b/frontend/src/routes/(root)/(logged)/workers/+page.svelte
index 78caf47cf1..5fa5ef7f56 100644
--- a/frontend/src/routes/(root)/(logged)/workers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/workers/+page.svelte
@@ -602,9 +602,12 @@
if (!config || !config.worker_tags || config.worker_tags.length === 0) cols += 1 // Worker tags
if ((!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole))
cols += 2 // Last job, Occupancy rate
- if ($superadmin || $devopsRole) cols += 1 // Repl
+ if (replAvailable) cols += 1 // Repl
return cols
})
+ // The REPL runs its commands as jobs in the selected workspace, which a devops user who
+ // belongs to no workspace does not have (see `isInstanceRoute`).
+ let replAvailable = $derived(($superadmin || $devopsRole) && !!$workspaceStore)
{#if $superadmin || $devopsRole}
@@ -1022,7 +1025,7 @@
LimitsVersionStatus
- {#if $superadmin || $devopsRole}
+ {#if replAvailable}
Repl
@@ -1152,13 +1155,17 @@
{jobs_executed}
{#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)}
- {#if last_job_id}
+
+ {#if last_job_id && $workspaceStore}
View last job
{last_job_workspace_id}
+ {:else if last_job_id}
+ {last_job_workspace_id}
{/if}
@@ -1277,7 +1284,7 @@