Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ declare module 'vue' {
AuthOrchestrator: typeof import('./src/components/auth/AuthOrchestrator.vue')['default']
DropField: typeof import('./src/components/DropField.vue')['default']
EditableText: typeof import('./src/components/EditableText.vue')['default']
ErrorIndicator: typeof import('./src/components/ErrorIndicator.vue')['default']
IBiArrowDownCircleFill: typeof import('~icons/bi/arrow-down-circle-fill')['default']
IBiArrowLeft: typeof import('~icons/bi/arrow-left')['default']
IBiArrowRightCircle: typeof import('~icons/bi/arrow-right-circle')['default']
IBiCheck2: typeof import('~icons/bi/check2')['default']
IBiChevronRight: typeof import('~icons/bi/chevron-right')['default']
IBiClipboard: typeof import('~icons/bi/clipboard')['default']
IBiCompassFill: typeof import('~icons/bi/compass-fill')['default']
IBiExclamationTriangleFill: typeof import('~icons/bi/exclamation-triangle-fill')['default']
IBiPencilFill: typeof import('~icons/bi/pencil-fill')['default']
IBiTrash: typeof import('~icons/bi/trash')['default']
LoginFailureModal: typeof import('./src/components/auth/LoginFailureModal.vue')['default']
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/DropField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { reportError } from '@/stores/errorstore'
const emit = defineEmits(['files-dropped'])

function onDrop(e: DragEvent) {
if (e.dataTransfer === null) { throw new Error("e.dataTransfer is null") }
if (e.dataTransfer === null) { reportError("e.dataTransfer is null"); return }
const fileList = []
for (let i = 0; i < e.dataTransfer.files.length; i++) {
fileList.push(e.dataTransfer.files.item(i))
Expand Down
142 changes: 142 additions & 0 deletions client/src/components/ErrorIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { BModal, BButton } from 'bootstrap-vue-next'
import { useErrorStore } from '@/stores/errorstore'
import type { ErrorEntry } from '@/stores/errorstore'

const errorStore = useErrorStore()

const overlayVisible = ref(false)
const confirmDismissVisible = ref(false)
const copiedId = ref<number | null>(null)
const copiedAll = ref(false)

const displayCount = computed(() =>
errorStore.errorCount > 99 ? '99+' : String(errorStore.errorCount)
)

function formatEntry(e: ErrorEntry): string {
return `[${e.timestamp.toISOString()}] ${e.message}` +
(e.detail ? `\n${e.detail}` : '')
}

async function copyOne(e: ErrorEntry) {
await navigator.clipboard.writeText(formatEntry(e))
copiedId.value = e.id
setTimeout(() => { copiedId.value = null }, 1500)
}

async function copyAll() {
await navigator.clipboard.writeText(
errorStore.errors.map(formatEntry).join('\n\n---\n\n')
)
copiedAll.value = true
setTimeout(() => { copiedAll.value = false }, 1500)
}

function confirmedDismissAll() {
errorStore.dismissAll()
confirmDismissVisible.value = false
overlayVisible.value = false
}
</script>

<template>
<button v-if="errorStore.hasErrors" v-show="!overlayVisible" class="error-fab btn btn-danger shadow-lg"
:aria-label="`${errorStore.errorCount} errors — click to view`" @click="overlayVisible = true">
<i-bi-exclamation-triangle-fill />
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning text-dark">
{{ displayCount }}
</span>
</button>

<BModal v-model="overlayVisible" title="Error Log" size="lg" scrollable>
<div v-for="entry in errorStore.errors" :key="entry.id"
class="d-flex align-items-start gap-2 mb-2 p-2 border rounded">
<div class="flex-grow-1 font-monospace small text-break">
<div>{{ entry.message }}</div>
<div v-if="entry.detail" class="text-muted small mt-1" style="max-height: 3em; overflow: hidden">
{{ entry.detail }}
</div>
<div class="text-muted" style="font-size: 0.7em">
{{ entry.timestamp.toLocaleTimeString() }}
</div>
</div>
<button class="btn btn-sm btn-outline-secondary flex-shrink-0"
:title="copiedId === entry.id ? 'Copied!' : 'Copy message'" @click="copyOne(entry)">
<i-bi-check2 v-if="copiedId === entry.id" />
<i-bi-clipboard v-else />
</button>
<button class="btn-close flex-shrink-0 mt-1" aria-label="Dismiss" @click="errorStore.dismissError(entry.id)" />
</div>
<template #footer>
<BButton variant="outline-secondary" class="me-auto" @click="copyAll">
<i-bi-check2 v-if="copiedAll" />
<i-bi-clipboard v-else />
Copy All
</BButton>
<BButton variant="outline-danger" @click="confirmDismissVisible = true">
<i-bi-trash />
Clear All
</BButton>
<BButton variant="secondary" @click="overlayVisible = false">
Close
</BButton>
</template>
</BModal>

<BModal v-model="confirmDismissVisible" title="Clear all errors?" size="sm">
<p>Dismiss all {{ errorStore.errorCount }} error message(s)?</p>
<template #footer>
<BButton variant="danger" @click="confirmedDismissAll">Clear All</BButton>
<BButton variant="secondary" @click="confirmDismissVisible = false">Cancel</BButton>
</template>
</BModal>
</template>

<style scoped>
.error-fab {
position: fixed;
bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
left: calc(1.5rem + env(safe-area-inset-left, 0px));
z-index: 1060;
width: 3.5rem;
height: 3.5rem;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}

/* Keep button inside Bootstrap container at each breakpoint */
@media (min-width: 576px) {
.error-fab {
left: calc((100vw - 540px) / 2 + 1.5rem);
}
}

@media (min-width: 768px) {
.error-fab {
left: calc((100vw - 720px) / 2 + 1.5rem);
}
}

@media (min-width: 992px) {
.error-fab {
left: calc((100vw - 960px) / 2 + 1.5rem);
}
}

@media (min-width: 1200px) {
.error-fab {
left: calc((100vw - 1140px) / 2 + 1.5rem);
}
}

@media (min-width: 1400px) {
.error-fab {
left: calc((100vw - 1320px) / 2 + 1.5rem);
}
}

</style>
9 changes: 5 additions & 4 deletions client/src/components/MapComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useMapStateStore } from '@/stores/mapstate'
import { StyleFactoryFixedColors, THREE_BROWN_COLORSTYLE, FIVE_COLORFUL_COLORSTYLE } from '@/lib/mapStyles';
import { queue, type QueueObject } from 'async'
import { createTrackLoadingAsyncWorker, type IdList, type Task } from '@/lib/trackLoadAsyncWorker'
import { reportError } from '@/stores/errorstore'

const NUMWORKERS = 4
const BATCHSIZE = 5
Expand Down Expand Up @@ -86,7 +87,7 @@ configStore.loadConfig(props.sid)
throw Error(`Unknown TRACKSTYLE config value ${TRACKSTYLE}`)
}
})
.catch((e) => console.error("Could not load config", e))
.catch((e) => reportError("Could not load config", e))

// Initialize loading worker and queue
const addLayerFunc = (featureWithTrack: MultiLineStringWithTrack) => mmap.addTrackLayer(featureWithTrack)
Expand Down Expand Up @@ -124,7 +125,7 @@ onMounted(() => {
() => { popoverData.value = null }
)
}).catch((err) => {
console.error("Error in nextTick", err)
reportError("Error in nextTick", err)
})
})

Expand Down Expand Up @@ -180,13 +181,13 @@ function makeVisible(ids: IdList, mmap: ManagedMap, queue: QueueObject<Task>, zo
if (zoomOut) { mmap.setExtentAndZoomOut() }
}
)
.catch((e) => console.error("what??", e))
.catch((e) => reportError("Error draining load queue", e))

loading.value = true // should be true already
queue.push<Task>(listOfTasks, (err, retVal) => {
if (err) {
if (err.name === 'AbortError') console.log("Aborted worker")
else console.error("Error in worker", err)
else reportError("Error in worker", err)
}
if (retVal) console.log("Return value from worker", retVal)
})
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/TrackManagerNavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<template>
<b-container id="root" class="d-flex flex-column vh-100 gx-0 border">
<auth-orchestrator />
<div class="bg-light d-flex flex-column vh-100">
<div class="bg-light d-flex flex-column vh-100 position-relative">
<nav class="navbar navbar-expand bg-success-subtle border-bottom py-1">
<div class="container-fluid">
<!-- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
Expand Down Expand Up @@ -54,6 +54,7 @@
</div>
</nav>
<slot></slot>
<ErrorIndicator />
</div>
</b-container>
</template>
Expand Down
5 changes: 3 additions & 2 deletions client/src/lib/auth/registration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import { getWithCORS, sendJSONToServer, getErrorMessage } from '@/lib/httpHelpers';
import { reportError } from '@/stores/errorstore';
import { startRegistration } from '@simplewebauthn/browser';
import type { RegistrationResponseJSON, PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/browser'

Expand Down Expand Up @@ -48,13 +49,13 @@ export async function registerPasskey(registrationKey: string): Promise<Registra
returnStatus.success = false
return returnStatus
} else {
console.error(getErrorMessage(error))
reportError(getErrorMessage(error))
returnStatus.message = "Failed on client side";
returnStatus.success = false
return returnStatus
}
} else {
console.error(getErrorMessage(error))
reportError(getErrorMessage(error))
returnStatus.message = "Failed on client side (2)"
returnStatus.success = false
return returnStatus
Expand Down
8 changes: 8 additions & 0 deletions client/src/lib/getconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ interface KeyValuePair {
}


/**
* Throws on invalid conftype/confkey, HTTP errors, or unexpected response shapes.
* Callers must handle rejection (e.g. `.catch()` with `reportError`).
*/
async function getConfig(sid: string, conftype: string, confkey: string): Promise<string> {

if (!conftypes.includes(conftype)) {
Expand Down Expand Up @@ -61,6 +65,10 @@ async function getConfig(sid: string, conftype: string, confkey: string): Promis
throw Error("Got wrong type from r.json()", { cause: rJson })
}

/**
* Throws on HTTP errors, unexpected response shapes, or schema validation failures.
* Callers must handle rejection (e.g. `.catch()` with `reportError`).
*/
async function getSchemaConfig(sid: string): Promise<Record<string, string>> {

const conftype = 'SCHEMA'
Expand Down
8 changes: 5 additions & 3 deletions client/src/lib/trackLoadAsyncWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { MultiLineStringWithTrack } from '@/lib/zodSchemas'
import type { MultiLineStringWithTrackId } from 'trackmanager-shared/zodSchemas'
import type { AsyncWorker } from 'async'
import { getTrackMetaDataByIdList, getMultiLineStringWithIdList } from './trackServices';
import { reportError } from '@/stores/errorstore';

type IdList = number[]
interface Task { idList: IdList, signal: AbortSignal }
Expand All @@ -27,7 +28,8 @@ function createTrackLoadingAsyncWorker(
// ----
const trackMetadataList = await getTrackMetaDataByIdList(idList, sid, signal)
if (trackMetadataList === null) {
throw Error("Track metadata list could not be loaded")
reportError("Track metadata list could not be loaded")
return
}
const trackMetadataMap = new Map<number, Track>()
trackMetadataList.forEach((track) => trackMetadataMap.set(track.id, track))
Expand All @@ -42,10 +44,10 @@ function createTrackLoadingAsyncWorker(
if (signal.aborted) return
idList.forEach((id) => {
const track = trackMetadataMap.get(id)
if (track === undefined) throw Error(`Track for id ${id} is undefined`)
if (track === undefined) { reportError(`Track for id ${id} is undefined`); return }

const geoJsonWithId = geoJsonMap.get(id)
if (geoJsonWithId === undefined) throw Error(`GeoJson for id ${id} is undefined`)
if (geoJsonWithId === undefined) { reportError(`GeoJson for id ${id} is undefined`); return }

const geoJsonWithTrack: MultiLineStringWithTrack = {
track,
Expand Down
Loading