Files
agentic-mobile-control/cloud-console/src/App.vue
T
q792602257andClaude Opus 4.6 fd0ea3a066
Tests / Test passed: 855
feat(console): Cloud Console Skills management view
Adds SkillsView.vue (cloud-skill CRUD, per-host entitlement grant/revoke,
read-only host local-skill inventory), skill API client methods + types,
and wires it into App.vue behind the skills:admin scope. Console
typecheck/build/tests green (20 passed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-15 08:06:49 +08:00

181 lines
5.6 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import type { Component } from "vue";
import {
Boxes,
BookOpen,
ListChecks,
LogOut,
MonitorSmartphone,
Puzzle,
SlidersHorizontal,
UsersRound,
} from "@lucide/vue";
import {
AUTH_INVALID_EVENT,
getCurrentUser,
logout,
} from "./api";
import { hasScope } from "./permissions";
import type { CloudUser } from "./types";
import LoginScreen from "./views/LoginScreen.vue";
import PasswordChangeScreen from "./views/PasswordChangeScreen.vue";
import TasksView from "./views/TasksView.vue";
import DevicesView from "./views/DevicesView.vue";
import PluginsView from "./views/PluginsView.vue";
import UsersView from "./views/UsersView.vue";
import LlmProvidersView from "./views/LlmProvidersView.vue";
import SkillsView from "./views/SkillsView.vue";
type ViewId = "tasks" | "devices" | "plugins" | "users" | "providers" | "skills";
const activeView = ref<ViewId>("tasks");
const currentUser = ref<CloudUser | null>(null);
const loading = ref(true);
const authMessage = ref("");
const canAdminPlugins = computed(
() =>
currentUser.value?.scopes.includes("*") ||
currentUser.value?.scopes.includes("plugins:admin"),
);
const canSubmitTasks = computed(
() =>
currentUser.value?.scopes.includes("*") ||
currentUser.value?.scopes.includes("tasks:submit"),
);
const canAdminUsers = computed(
() => Boolean(
currentUser.value?.scopes.includes("*") ||
currentUser.value?.scopes.includes("users:admin")),
);
const canAdminGovernance = computed(
() => Boolean(
currentUser.value?.scopes.includes("*") ||
currentUser.value?.scopes.includes("governance:admin")),
);
const canAdminProviders = computed(() => hasScope(currentUser.value, "llm-providers:admin"));
const canAdminSkills = computed(() => hasScope(currentUser.value, "skills:admin"));
const isAuthenticated = computed(() => currentUser.value !== null);
const currentUserLabel = computed(() =>
currentUser.value ? `${currentUser.value.display_name} (${currentUser.value.role})` : "",
);
const mustChangePassword = computed(() => currentUser.value?.must_change_password ?? false);
const navItems = computed<{ id: ViewId; label: string; icon: Component }[]>(() => {
const items: { id: ViewId; label: string; icon: Component }[] = [
{ id: "tasks", label: "Tasks", icon: ListChecks },
{ id: "devices", label: "Devices", icon: MonitorSmartphone },
{ id: "plugins", label: "Plugins", icon: Puzzle },
];
if (canAdminUsers.value || canAdminGovernance.value) {
items.push({ id: "users", label: "Users & limits", icon: UsersRound });
}
if (canAdminProviders.value) {
items.push({ id: "providers", label: "LLM providers", icon: SlidersHorizontal });
}
if (canAdminSkills.value) {
items.push({ id: "skills", label: "Skills", icon: BookOpen });
}
return items;
});
async function initializeAuthentication() {
loading.value = true;
currentUser.value = null;
try {
currentUser.value = await getCurrentUser();
} catch {
// A missing session is the normal initial state.
}
loading.value = false;
}
async function onAuthenticated() {
authMessage.value = "";
await initializeAuthentication();
}
function onAuthInvalid() {
currentUser.value = null;
authMessage.value = "your session expired. sign in again.";
}
async function signOut() {
try {
if (currentUser.value) await logout();
} catch {
// Local state must still be cleared when the already-expired session rejects logout.
}
currentUser.value = null;
authMessage.value = "";
}
function onPasswordChanged() {
currentUser.value = null;
authMessage.value = "password changed. sign in with the new password.";
}
onMounted(() => {
void initializeAuthentication();
window.addEventListener(AUTH_INVALID_EVENT, onAuthInvalid as EventListener);
});
onUnmounted(() => {
window.removeEventListener(AUTH_INVALID_EVENT, onAuthInvalid as EventListener);
});
const activeComponent = computed(() => {
switch (activeView.value) {
case "devices":
return DevicesView;
case "plugins":
return PluginsView;
case "users":
return UsersView;
case "providers":
return LlmProvidersView;
case "skills":
return SkillsView;
default:
return TasksView;
}
});
</script>
<template>
<div v-if="loading" class="token-screen"><p>Checking session</p></div>
<LoginScreen v-else-if="!isAuthenticated" :message="authMessage" @authenticated="onAuthenticated" />
<PasswordChangeScreen
v-else-if="mustChangePassword"
@changed="onPasswordChanged"
@sign-out="signOut"
/>
<div v-else class="app-shell">
<nav class="app-nav">
<h1><Boxes :size="14" /> Cloud Console</h1>
<button
v-for="item in navItems"
:key="item.id"
:class="{ active: activeView === item.id }"
@click="activeView = item.id"
>
<component :is="item.icon" :size="14" /> {{ item.label }}
</button>
<div class="spacer" />
<div class="dim">{{ currentUserLabel }}</div>
<button @click="signOut"><LogOut :size="14" /> Sign out</button>
</nav>
<main class="app-main">
<PluginsView v-if="activeView === 'plugins'" :can-admin="canAdminPlugins" />
<UsersView
v-else-if="activeView === 'users'"
:can-admin-users="canAdminUsers"
:can-admin-governance="canAdminGovernance"
/>
<LlmProvidersView v-else-if="activeView === 'providers'" :can-admin="canAdminProviders" />
<SkillsView v-else-if="activeView === 'skills'" :can-admin="canAdminSkills" />
<component v-else :is="activeComponent" :can-submit="canSubmitTasks" />
</main>
</div>
</template>