diff --git a/dashboard/src/routes/scripts/[id]/+page.svelte b/dashboard/src/routes/scripts/[id]/+page.svelte
index 640fcd5..75f5897 100644
--- a/dashboard/src/routes/scripts/[id]/+page.svelte
+++ b/dashboard/src/routes/scripts/[id]/+page.svelte
@@ -6,12 +6,15 @@
api,
ApiError,
type AppDomain,
+ type AppRole,
type ExecutionLog,
type Route,
type RouteInput,
type Script,
type VersionInfo
} from '$lib/api';
+ import { currentUser } from '$lib/auth';
+ import { canAdminApp, canWriteApp } from '$lib/capabilities';
import { logLevelColor, statusColor } from '$lib/styles';
import {
checkHostAgainstClaims,
@@ -47,6 +50,11 @@
let appSlug = $state(null);
let appDomains = $state([]);
+ let appMyRole = $state(null);
+
+ const me = $derived($currentUser);
+ const canWrite = $derived(canWriteApp(me, appMyRole));
+ const canAdmin = $derived(canAdminApp(me, appMyRole));
async function loadScript() {
scriptLoading = true;
@@ -58,15 +66,16 @@
editableDescription = script.description ?? '';
editableTimeout = script.timeout_seconds;
editableSandbox = { ...(script.sandbox ?? {}) };
- // Resolve the owning app's slug for the breadcrumb and its
- // domain claims for the route form's suggestions + live
- // validation. Both are non-fatal — the page works without
- // them.
+ // Resolve the owning app for the breadcrumb (slug),
+ // route-form host suggestions (domain claims), and UI
+ // shadowing (my_role on this app). All non-fatal — the
+ // page renders without them, just with reduced fidelity.
const appId = script.app_id;
void api.apps
.get(appId)
.then((a) => {
appSlug = a.slug;
+ appMyRole = a.my_role ?? null;
})
.catch(() => {});
void api.domains
@@ -386,6 +395,15 @@
void loadRoutes();
void loadLogs();
});
+
+ // Defense-in-depth: anyone non-admin who lands on the Settings
+ // tab via a stale link gets bounced back to Edit. The tab button
+ // itself is also hidden.
+ $effect(() => {
+ if (!canAdmin && tab === 'settings') {
+ tab = 'edit';
+ }
+ });
@@ -410,9 +428,11 @@
v{script.version} · timeout {script.timeout_seconds}s · {script.description ?? 'no description'}
-
+ {#if canAdmin}
+
+ {/if}