minor: heavily simplify server and app content. unify styling (#4)
All checks were successful
ci/woodpecker/push/push Pipeline was successful
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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ?? [])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user