minor: heavily simplify server and app content. unify styling (#4)
All checks were successful
ci/woodpecker/push/push Pipeline was successful

Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-02-14 04:52:18 +00:00
parent 1a143d2f8e
commit 17f28401ba
40 changed files with 595 additions and 933 deletions

View File

@@ -1,15 +1,14 @@
<template>
<div class="p-6">
<h2 class="mb-4 text-xl font-semibold tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<h2 class="kestrel-page-heading mb-4">
Account
</h2>
<!-- Profile -->
<section class="mb-8">
<h3 class="mb-2 text-sm font-medium uppercase tracking-wider text-kestrel-muted">
<h3 class="kestrel-section-label">
Profile
</h3>
<div class="rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow [box-shadow:0_0_20px_-4px_rgba(34,201,201,0.15)]">
<div class="kestrel-card p-4">
<template v-if="user">
<dl class="space-y-2 text-sm">
<div>
@@ -50,15 +49,14 @@
</div>
</section>
<!-- Change password (local only) -->
<section
v-if="user?.auth_provider === 'local'"
class="mb-8"
>
<h3 class="mb-2 text-sm font-medium uppercase tracking-wider text-kestrel-muted">
<h3 class="kestrel-section-label">
Change password
</h3>
<div class="rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow [box-shadow:0_0_20px_-4px_rgba(34,201,201,0.15)]">
<div class="kestrel-card p-4">
<p
v-if="passwordSuccess"
class="mb-3 text-sm text-green-400"
@@ -78,46 +76,40 @@
<div>
<label
for="account-current-password"
class="mb-1 block text-xs text-kestrel-muted"
>
Current password
</label>
class="kestrel-label"
>Current password</label>
<input
id="account-current-password"
v-model="currentPassword"
type="password"
autocomplete="current-password"
class="w-full rounded border border-kestrel-border bg-kestrel-bg px-3 py-2 text-sm text-kestrel-text outline-none focus:border-kestrel-accent"
class="kestrel-input"
>
</div>
<div>
<label
for="account-new-password"
class="mb-1 block text-xs text-kestrel-muted"
>
New password
</label>
class="kestrel-label"
>New password</label>
<input
id="account-new-password"
v-model="newPassword"
type="password"
autocomplete="new-password"
class="w-full rounded border border-kestrel-border bg-kestrel-bg px-3 py-2 text-sm text-kestrel-text outline-none focus:border-kestrel-accent"
class="kestrel-input"
>
</div>
<div>
<label
for="account-confirm-password"
class="mb-1 block text-xs text-kestrel-muted"
>
Confirm new password
</label>
class="kestrel-label"
>Confirm new password</label>
<input
id="account-confirm-password"
v-model="confirmPassword"
type="password"
autocomplete="new-password"
class="w-full rounded border border-kestrel-border bg-kestrel-bg px-3 py-2 text-sm text-kestrel-text outline-none focus:border-kestrel-accent"
class="kestrel-input"
>
</div>
<button

View File

@@ -1,6 +1,6 @@
<template>
<div class="p-6">
<h2 class="mb-4 text-xl font-semibold tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<h2 class="kestrel-page-heading mb-4">
Cameras
</h2>
<p class="mb-4 text-sm text-kestrel-muted">
@@ -80,6 +80,8 @@
<script setup>
definePageMeta({ layout: 'default' })
const { cameras } = useCameras()
const { cameras, liveSessions } = useCameras()
const selectedCamera = ref(null)
useAutoCloseLiveSession(selectedCamera, liveSessions)
</script>

View File

@@ -28,7 +28,8 @@ const { canEditPois } = useUser()
const selectedCamera = ref(null)
function onSelectLive(session) {
const latest = (liveSessions.value || []).find(s => s.id === session?.id)
selectedCamera.value = latest ?? session
selectedCamera.value = (liveSessions.value ?? []).find(s => s.id === session?.id) ?? session
}
useAutoCloseLiveSession(selectedCamera, liveSessions)
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex min-h-[60vh] items-center justify-center p-6">
<div class="w-full max-w-sm rounded border border-kestrel-border bg-kestrel-surface p-6 shadow-glow [box-shadow:0_0_20px_-4px_rgba(34,201,201,0.15)]">
<h2 class="mb-4 text-lg font-semibold text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<div class="kestrel-card w-full max-w-sm p-6">
<h2 class="kestrel-section-heading mb-4">
Sign in
</h2>
<p
@@ -29,28 +29,28 @@
<div class="mb-3">
<label
for="login-identifier"
class="mb-1 block text-xs text-kestrel-muted"
class="kestrel-label"
>Email or username</label>
<input
id="login-identifier"
v-model="identifier"
type="text"
autocomplete="username"
class="w-full rounded border border-kestrel-border bg-kestrel-bg px-3 py-2 text-sm text-kestrel-text outline-none focus:border-kestrel-accent"
class="kestrel-input"
required
>
</div>
<div class="mb-4">
<label
for="login-password"
class="mb-1 block text-xs text-kestrel-muted"
class="kestrel-label"
>Password</label>
<input
id="login-password"
v-model="password"
type="password"
autocomplete="current-password"
class="w-full rounded border border-kestrel-border bg-kestrel-bg px-3 py-2 text-sm text-kestrel-text outline-none focus:border-kestrel-accent"
class="kestrel-input"
required
>
</div>
@@ -69,16 +69,16 @@
<script setup>
const route = useRoute()
const redirect = computed(() => route.query.redirect || '/')
const AUTH_CONFIG_DEFAULT = Object.freeze({ oidc: { enabled: false, label: '' } })
const { data: authConfig } = useAsyncData(
'auth-config',
() => $fetch('/api/auth/config').catch(() => ({ oidc: { enabled: false, label: '' } })),
() => $fetch('/api/auth/config').catch(() => AUTH_CONFIG_DEFAULT),
{ default: () => null },
)
const showDivider = computed(() => !!authConfig.value?.oidc?.enabled)
const oidcAuthorizeUrl = computed(() => {
const base = '/api/auth/oidc/authorize'
const q = redirect.value && redirect.value !== '/' ? `?redirect=${encodeURIComponent(redirect.value)}` : ''
return base + q
const r = redirect.value
return `/api/auth/oidc/authorize${r && r !== '/' ? `?redirect=${encodeURIComponent(r)}` : ''}`
})
const identifier = ref('')

View File

@@ -1,6 +1,6 @@
<template>
<div class="p-6">
<h2 class="mb-2 text-xl font-semibold tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<h2 class="kestrel-page-heading mb-2">
Members
</h2>
<p
@@ -10,7 +10,7 @@
Sign in to view members.
</p>
<p
v-else-if="!canViewMembers"
v-else-if="!canEditPois"
class="text-sm text-kestrel-muted"
>
You don't have access to the members list.
@@ -149,7 +149,7 @@
v-if="openRoleDropdownId && dropdownPlacement"
ref="dropdownMenuRef"
role="menu"
class="fixed z-[100] min-w-[6rem] rounded border border-kestrel-border bg-kestrel-surface py-1 shadow-glow [box-shadow:0_4px_12px_-2px_rgba(34,201,201,0.15)]"
class="fixed z-[100] min-w-[6rem] rounded border border-kestrel-border bg-kestrel-surface py-1 shadow-glow shadow-glow-dropdown"
:style="{
top: `${dropdownPlacement.top}px`,
left: `${dropdownPlacement.left}px`,
@@ -183,7 +183,7 @@
@click.self="closeAddUserModal"
>
<div
class="w-full max-w-sm rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow"
class="kestrel-card-modal w-full max-w-sm p-4"
@click.stop
>
<h3
@@ -204,7 +204,7 @@
type="text"
required
autocomplete="username"
class="rounded border border-kestrel-border bg-kestrel-bg px-2 py-1.5 text-sm text-kestrel-text"
class="kestrel-input"
placeholder="username"
>
</div>
@@ -219,7 +219,7 @@
type="password"
required
autocomplete="new-password"
class="rounded border border-kestrel-border bg-kestrel-bg px-2 py-1.5 text-sm text-kestrel-text"
class="kestrel-input"
placeholder="••••••••"
>
</div>
@@ -231,7 +231,7 @@
<select
id="add-role"
v-model="newUser.role"
class="rounded border border-kestrel-border bg-kestrel-bg px-2 py-1.5 text-sm text-kestrel-text"
class="kestrel-input"
>
<option value="member">
member
@@ -253,7 +253,7 @@
<div class="flex justify-end gap-2">
<button
type="button"
class="rounded border border-kestrel-border px-3 py-1.5 text-sm text-kestrel-text hover:bg-kestrel-surface-hover"
class="kestrel-btn-secondary"
@click="closeAddUserModal"
>
Cancel
@@ -280,7 +280,7 @@
@click.self="deleteConfirmUser = null"
>
<div
class="w-full max-w-sm rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow"
class="kestrel-card-modal w-full max-w-sm p-4"
@click.stop
>
<h3
@@ -295,7 +295,7 @@
<div class="flex justify-end gap-2">
<button
type="button"
class="rounded border border-kestrel-border px-3 py-1.5 text-sm text-kestrel-text hover:bg-kestrel-surface-hover"
class="kestrel-btn-secondary"
@click="deleteConfirmUser = null"
>
Cancel
@@ -318,7 +318,7 @@
@click.self="editUserModal = null"
>
<div
class="w-full max-w-sm rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow"
class="kestrel-card-modal w-full max-w-sm p-4"
role="dialog"
aria-modal="true"
aria-labelledby="edit-user-title"
@@ -340,7 +340,7 @@
v-model="editForm.identifier"
type="text"
required
class="rounded border border-kestrel-border bg-kestrel-bg px-2 py-1.5 text-sm text-kestrel-text"
class="kestrel-input"
>
</div>
<div class="mb-4 flex flex-col gap-1">
@@ -353,7 +353,7 @@
v-model="editForm.password"
type="password"
autocomplete="new-password"
class="rounded border border-kestrel-border bg-kestrel-bg px-2 py-1.5 text-sm text-kestrel-text"
class="kestrel-input"
placeholder="••••••••"
>
<p class="mt-0.5 text-xs text-kestrel-muted">
@@ -369,7 +369,7 @@
<div class="flex justify-end gap-2">
<button
type="button"
class="rounded border border-kestrel-border px-3 py-1.5 text-sm text-kestrel-text hover:bg-kestrel-surface-hover"
class="kestrel-btn-secondary"
@click="editUserModal = null"
>
Cancel
@@ -390,15 +390,14 @@
</template>
<script setup>
const { user, isAdmin, refresh: refreshUser } = useUser()
const canViewMembers = computed(() => user.value?.role === 'admin' || user.value?.role === 'leader')
const { user, isAdmin, canEditPois, refresh: refreshUser } = useUser()
const { data: usersData, refresh: refreshUsers } = useAsyncData(
'users',
() => $fetch('/api/users').catch(() => []),
{ default: () => [] },
)
const users = computed(() => (Array.isArray(usersData.value) ? usersData.value : []))
const users = computed(() => Object.freeze([...(usersData.value ?? [])]))
const roleOptions = ['admin', 'leader', 'member']
const pendingRoleUpdates = ref({})
@@ -428,8 +427,8 @@ function setDropdownWrapRef(userId, el) {
}
}
watch(user, (u) => {
if (u?.role === 'admin' || u?.role === 'leader') refreshUsers()
watch(user, () => {
if (canEditPois.value) refreshUsers()
}, { immediate: true })
function toggleRoleDropdown(userId) {

View File

@@ -1,6 +1,6 @@
<template>
<div class="p-6">
<h2 class="mb-2 text-xl font-semibold tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<h2 class="kestrel-page-heading mb-2">
POI placement
</h2>
<p
@@ -17,7 +17,7 @@
<div>
<label
for="poi-lat"
class="mb-1 block text-xs text-kestrel-muted"
class="kestrel-label"
>Lat</label>
<input
id="poi-lat"
@@ -25,13 +25,13 @@
type="number"
step="any"
required
class="w-28 rounded border border-kestrel-border bg-kestrel-bg px-2 py-1 text-sm text-kestrel-text"
class="kestrel-input w-28"
>
</div>
<div>
<label
for="poi-lng"
class="mb-1 block text-xs text-kestrel-muted"
class="kestrel-label"
>Lng</label>
<input
id="poi-lng"
@@ -39,39 +39,37 @@
type="number"
step="any"
required
class="w-28 rounded border border-kestrel-border bg-kestrel-bg px-2 py-1 text-sm text-kestrel-text"
class="kestrel-input w-28"
>
</div>
<div>
<label
for="poi-label"
class="mb-1 block text-xs text-kestrel-muted"
class="kestrel-label"
>Label</label>
<input
id="poi-label"
v-model="form.label"
type="text"
class="w-40 rounded border border-kestrel-border bg-kestrel-bg px-2 py-1 text-sm text-kestrel-text"
class="kestrel-input w-40"
>
</div>
<div>
<label
for="poi-icon"
class="mb-1 block text-xs text-kestrel-muted"
class="kestrel-label"
>Icon</label>
<select
id="poi-icon"
v-model="form.iconType"
class="rounded border border-kestrel-border bg-kestrel-bg px-2 py-1 text-sm text-kestrel-text"
class="kestrel-input w-28"
>
<option value="pin">
pin
</option>
<option value="flag">
flag
</option>
<option value="waypoint">
waypoint
<option
v-for="opt in POI_ICON_TYPES"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</div>
@@ -145,6 +143,8 @@
</template>
<script setup>
const POI_ICON_TYPES = Object.freeze(['pin', 'flag', 'waypoint'])
const { data: poisData, refresh } = usePois()
const { canEditPois } = useUser()
const poisList = computed(() => poisData.value ?? [])

View File

@@ -1,15 +1,14 @@
<template>
<div class="p-6">
<h2 class="mb-4 text-xl font-semibold tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<h2 class="kestrel-page-heading mb-4">
Settings
</h2>
<!-- Map & offline -->
<section class="mb-8">
<h3 class="mb-2 text-sm font-medium uppercase tracking-wider text-kestrel-muted">
<h3 class="kestrel-section-label">
Map & offline
</h3>
<div class="rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow [box-shadow:0_0_20px_-4px_rgba(34,201,201,0.15)]">
<div class="kestrel-card p-4">
<p class="mb-3 text-sm text-kestrel-text">
Clear saved map tiles to free storage. The map will load tiles from the network again when you use it.
</p>
@@ -28,7 +27,7 @@
</p>
<button
type="button"
class="rounded border border-kestrel-border px-4 py-2 text-sm text-kestrel-text transition-colors hover:bg-kestrel-border disabled:opacity-50"
class="kestrel-btn-secondary disabled:opacity-50"
:disabled="tilesLoading"
@click="onClearTiles"
>
@@ -37,12 +36,11 @@
</div>
</section>
<!-- About -->
<section>
<h3 class="mb-2 text-sm font-medium uppercase tracking-wider text-kestrel-muted">
<h3 class="kestrel-section-label">
About
</h3>
<div class="rounded border border-kestrel-border bg-kestrel-surface p-4 shadow-glow [box-shadow:0_0_20px_-4px_rgba(34,201,201,0.15)]">
<div class="kestrel-card p-4">
<p class="font-medium text-kestrel-text">
KestrelOS
</p>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex min-h-[80vh] flex-col items-center justify-center p-6">
<div class="w-full max-w-md rounded-lg border border-kestrel-border bg-kestrel-surface p-6 shadow-glow [box-shadow:0_0_24px_-6px_rgba(34,201,201,0.2)]">
<h2 class="mb-2 text-lg font-semibold tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
<div class="kestrel-card-modal w-full max-w-md p-6">
<h2 class="kestrel-section-heading mb-2">
Share live (camera + location)
</h2>
<p class="mb-4 text-sm text-kestrel-muted">
@@ -55,7 +55,7 @@
<!-- Local preview -->
<div
v-if="stream && videoRef"
class="relative mb-4 aspect-video w-full overflow-hidden rounded border border-kestrel-border bg-black"
class="kestrel-video-frame mb-4"
>
<video
ref="videoRef"