testing claude code

This commit is contained in:
Geir Okkenhaug Jerstad 2025-06-25 16:36:30 +02:00
parent 1ad4663e6e
commit a7660d0b8d
66 changed files with 10837 additions and 457 deletions

View file

@ -0,0 +1,87 @@
<template>
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo and title -->
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center">
<div class="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center">
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<h1 class="ml-3 text-xl font-semibold text-gray-900">
Home Lab Dashboard
</h1>
</div>
</div>
<!-- User menu -->
<div class="flex items-center space-x-4">
<!-- Refresh button -->
<button
type="button"
class="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
title="Refresh data"
@click="$emit('refresh')"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<!-- User info and logout -->
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2">
<div class="h-8 w-8 bg-gray-300 rounded-full flex items-center justify-center">
<span class="text-sm font-medium text-gray-700">
{{ userInitials }}
</span>
</div>
<div class="hidden md:block">
<p class="text-sm font-medium text-gray-900">{{ user.username }}</p>
<p class="text-xs text-gray-500">{{ user.email }}</p>
</div>
</div>
<button
type="button"
class="ml-3 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
@click="$emit('logout')"
>
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Sign out
</button>
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { User } from '@/stores/auth'
interface Props {
user: User
}
interface Emits {
(e: 'logout'): void
(e: 'refresh'): void
}
const props = defineProps<Props>()
defineEmits<Emits>()
const userInitials = computed(() => {
const names = props.user.username.split(' ')
if (names.length >= 2) {
return (names[0][0] + names[1][0]).toUpperCase()
}
return props.user.username.slice(0, 2).toUpperCase()
})
</script>

View file

@ -0,0 +1,217 @@
<template>
<aside class="fixed inset-y-0 left-0 z-50 w-64 bg-gray-900 overflow-y-auto">
<div class="flex flex-col h-full">
<!-- Sidebar header spacer -->
<div class="h-16 flex items-center px-4">
<!-- This matches the header height -->
</div>
<!-- Navigation -->
<nav class="flex-1 px-4 pb-4 space-y-2">
<div class="space-y-1">
<button
v-for="item in navigationItems"
:key="item.id"
type="button"
:class="[
'group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md transition-colors',
activeSection === item.id
? 'bg-gray-800 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
]"
@click="$emit('section-change', item.id)"
>
<component
:is="item.icon"
:class="[
'mr-3 h-5 w-5 flex-shrink-0',
activeSection === item.id ? 'text-white' : 'text-gray-400 group-hover:text-white'
]"
/>
{{ item.name }}
<!-- Badge for notifications -->
<span
v-if="item.badge"
:class="[
'ml-auto inline-block py-0.5 px-2 text-xs rounded-full',
item.badgeColor || 'bg-red-100 text-red-800'
]"
>
{{ item.badge }}
</span>
</button>
</div>
<!-- Section divider -->
<div class="border-t border-gray-700 pt-4 mt-4">
<h3 class="px-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">
Quick Links
</h3>
<div class="mt-2 space-y-1">
<a
v-for="link in quickLinks"
:key="link.name"
:href="link.href"
target="_blank"
rel="noopener noreferrer"
class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
>
<component
:is="link.icon"
class="mr-3 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-white"
/>
{{ link.name }}
<svg class="ml-auto h-4 w-4 text-gray-400 group-hover:text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" clip-rule="evenodd" />
<path fill-rule="evenodd" d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
</nav>
<!-- Footer -->
<div class="flex-shrink-0 px-4 py-4 border-t border-gray-700">
<p class="text-xs text-gray-400">
Last updated: {{ formatTime(new Date()) }}
</p>
</div>
</div>
</aside>
</template>
<script setup lang="ts">
import { h } from 'vue'
interface Props {
activeSection: string
}
interface Emits {
(e: 'section-change', section: string): void
}
defineProps<Props>()
defineEmits<Emits>()
// Simple icon components using h() function
const DashboardIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z'
}),
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M8 5v4M16 5v4'
})
])
const ServerIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01'
})
])
const ChartIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
})
])
const SettingsIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z'
}),
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z'
})
])
const ExternalLinkIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14'
})
])
const navigationItems = [
{
id: 'overview',
name: 'Overview',
icon: DashboardIcon
},
{
id: 'services',
name: 'Services',
icon: ServerIcon,
badge: '2',
badgeColor: 'bg-red-100 text-red-800'
},
{
id: 'monitoring',
name: 'Monitoring',
icon: ChartIcon
},
{
id: 'settings',
name: 'Settings',
icon: SettingsIcon
}
]
const quickLinks = [
{
name: 'Netdata',
href: 'http://localhost:19999',
icon: ExternalLinkIcon
},
{
name: 'Ollama',
href: 'http://localhost:11434',
icon: ExternalLinkIcon
}
]
const formatTime = (date: Date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
</script>

View file

@ -0,0 +1,46 @@
<template>
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
Error
</h3>
<div class="mt-2 text-sm text-red-700">
<p>{{ message }}</p>
</div>
</div>
<div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5">
<button
type="button"
class="inline-flex bg-red-50 rounded-md p-1.5 text-red-500 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-red-50 focus:ring-red-600"
@click="$emit('dismiss')"
>
<span class="sr-only">Dismiss</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
message: string
}
interface Emits {
(e: 'dismiss'): void
}
defineProps<Props>()
defineEmits<Emits>()
</script>

View file

@ -0,0 +1,43 @@
<template>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Load Average</dt>
<dd class="text-lg font-medium text-gray-900">{{ load[0].toFixed(2) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="grid grid-cols-3 gap-4 text-sm">
<div class="text-center">
<span class="block font-medium">{{ load[0].toFixed(2) }}</span>
<span class="text-gray-500">1m</span>
</div>
<div class="text-center">
<span class="block font-medium">{{ load[1].toFixed(2) }}</span>
<span class="text-gray-500">5m</span>
</div>
<div class="text-center">
<span class="block font-medium">{{ load[2].toFixed(2) }}</span>
<span class="text-gray-500">15m</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
load: readonly number[]
}
defineProps<Props>()
</script>

View file

@ -0,0 +1,13 @@
<template>
<div class="flex items-center justify-center space-x-2">
<svg class="animate-spin h-5 w-5 text-blue-500" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-gray-500">Loading...</span>
</div>
</template>
<script setup lang="ts">
// No props needed for this simple loading spinner
</script>

View file

@ -0,0 +1,124 @@
<template>
<form class="mt-8 space-y-6" @submit.prevent="handleSubmit">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="username" class="sr-only">Username</label>
<input
id="username"
v-model="form.username"
name="username"
type="text"
autocomplete="username"
required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Username"
:disabled="isLoading"
>
</div>
<div>
<label for="password" class="sr-only">Password</label>
<input
id="password"
v-model="form.password"
name="password"
type="password"
autocomplete="current-password"
required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
:disabled="isLoading"
>
</div>
</div>
<div v-if="error" class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
Login Failed
</h3>
<div class="mt-2 text-sm text-red-700">
<p>{{ error }}</p>
</div>
</div>
<div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5">
<button
type="button"
class="inline-flex bg-red-50 rounded-md p-1.5 text-red-500 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-red-50 focus:ring-red-600"
@click="$emit('clear-error')"
>
<span class="sr-only">Dismiss</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between">
<div class="text-sm">
<p class="text-gray-600">
Demo credentials: any username/password combination
</p>
</div>
</div>
<div>
<button
type="submit"
:disabled="isLoading || !form.username || !form.password"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
<svg v-if="!isLoading" class="h-5 w-5 text-blue-500 group-hover:text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
<svg v-else class="animate-spin h-5 w-5 text-blue-500" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
{{ isLoading ? 'Signing in...' : 'Sign in' }}
</button>
</div>
</form>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
interface Props {
isLoading: boolean
error: string | null
}
interface Emits {
(e: 'submit', credentials: { username: string; password: string }): void
(e: 'clear-error'): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const form = reactive({
username: '',
password: ''
})
const handleSubmit = () => {
if (form.username && form.password) {
emit('submit', {
username: form.username,
password: form.password
})
}
}
</script>

View file

@ -0,0 +1,143 @@
<template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<button
v-for="action in actions"
:key="action.id"
type="button"
:class="[
'relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-6 py-5 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
action.enabled
? 'hover:border-gray-400 cursor-pointer'
: 'opacity-50 cursor-not-allowed'
]"
:disabled="!action.enabled"
@click="handleAction(action.id)"
>
<div class="flex-shrink-0">
<component
:is="action.icon"
:class="[
'h-6 w-6',
action.enabled ? action.color : 'text-gray-400'
]"
/>
</div>
<div class="min-w-0 flex-1">
<span class="absolute inset-0" aria-hidden="true"></span>
<p class="text-sm font-medium text-gray-900">{{ action.name }}</p>
<p class="text-sm text-gray-500 truncate">{{ action.description }}</p>
</div>
</button>
</div>
</template>
<script setup lang="ts">
import { h } from 'vue'
interface Emits {
(e: 'action', actionId: string): void
}
const emit = defineEmits<Emits>()
// Icon components
const RefreshIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15'
})
])
const TerminalIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z'
})
])
const ChartIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
})
])
const SettingsIcon = () => h('svg', {
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z'
}),
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z'
})
])
const actions = [
{
id: 'refresh',
name: 'Refresh All',
description: 'Update all service status',
icon: RefreshIcon,
color: 'text-blue-600',
enabled: true
},
{
id: 'netdata',
name: 'Open Netdata',
description: 'View detailed metrics',
icon: ChartIcon,
color: 'text-green-600',
enabled: true
},
{
id: 'terminal',
name: 'SSH Access',
description: 'Connect via terminal',
icon: TerminalIcon,
color: 'text-gray-600',
enabled: false
},
{
id: 'settings',
name: 'System Settings',
description: 'Configure services',
icon: SettingsIcon,
color: 'text-purple-600',
enabled: false
}
]
const handleAction = (actionId: string) => {
const action = actions.find(a => a.id === actionId)
if (action?.enabled) {
emit('action', actionId)
}
}
</script>

View file

@ -0,0 +1,46 @@
<template>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Service Status</dt>
<dd class="text-lg font-medium text-gray-900">{{ totalCount }} Services</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="grid grid-cols-3 gap-4 text-sm">
<div class="text-center">
<span class="block text-lg font-medium text-green-600">{{ runningCount }}</span>
<span class="text-gray-500">Running</span>
</div>
<div class="text-center">
<span class="block text-lg font-medium text-red-600">{{ errorCount }}</span>
<span class="text-gray-500">Errors</span>
</div>
<div class="text-center">
<span class="block text-lg font-medium text-yellow-600">{{ stoppedCount }}</span>
<span class="text-gray-500">Stopped</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
runningCount: number
stoppedCount: number
errorCount: number
totalCount: number
}
defineProps<Props>()
</script>

View file

@ -0,0 +1,107 @@
<template>
<div class="overflow-hidden">
<div class="flex justify-between items-center mb-4">
<h4 class="text-sm font-medium text-gray-900">Service Status</h4>
<button
type="button"
class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
@click="$emit('refresh')"
>
<svg class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Service
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Port
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="service in services" :key="service.name">
<td class="px-6 py-4 whitespace-nowrap">
<div>
<div class="text-sm font-medium text-gray-900">{{ service.name }}</div>
<div v-if="service.description" class="text-sm text-gray-500">{{ service.description }}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getStatusClass(service.status)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
{{ service.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ service.port || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<a
v-if="service.url"
:href="service.url"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-900 mr-3"
>
Open
</a>
<button
type="button"
class="text-gray-600 hover:text-gray-900"
@click="showServiceDetails(service)"
>
Details
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import type { ServiceStatus } from '@/stores/dashboard'
interface Props {
services: readonly ServiceStatus[]
}
interface Emits {
(e: 'refresh'): void
}
defineProps<Props>()
defineEmits<Emits>()
const getStatusClass = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800'
case 'stopped':
return 'bg-yellow-100 text-yellow-800'
case 'error':
return 'bg-red-100 text-red-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
const showServiceDetails = (service: ServiceStatus) => {
// Placeholder for service details functionality
console.log('Show details for:', service.name)
}
</script>

View file

@ -0,0 +1,43 @@
<template>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">System Information</dt>
<dd class="text-lg font-medium text-gray-900">{{ systemInfo.hostname }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500">Memory:</span>
<span class="ml-2 font-medium">{{ memoryUsage }}%</span>
</div>
<div>
<span class="text-gray-500">Disk:</span>
<span class="ml-2 font-medium">{{ diskUsage }}%</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { SystemInfo } from '@/stores/dashboard'
interface Props {
systemInfo: SystemInfo
memoryUsage: number
diskUsage: number
}
defineProps<Props>()
</script>

View file

@ -1,94 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View file

@ -0,0 +1,33 @@
<template>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">System Uptime</dt>
<dd class="text-lg font-medium text-gray-900">{{ uptime }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm text-gray-500">
Host: <span class="font-medium text-gray-900">{{ hostname }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
uptime: string
hostname: string
}
defineProps<Props>()
</script>

View file

@ -1,87 +0,0 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>