All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Full-site fake-data audit findings: - SetupWizard showed a curl|sh installer (get.corrosionmgmt.com) and a 'corrosion-agent' binary that don't exist -> real host-agent commands - 'View live demo' CTA on 5 marketing pages linked to a login, not a demo -> honest 'Sign in' - Google Fonts @import was silently dropped from the production CSS bundle (mid-bundle @import) -> <link> tags in index.html; prod was shipping system fallback fonts - App-root ErrorBoundary bricked the entire SPA (incl. marketing) on a single failed fetch until manual reload -> resets on route change + content-scoped boundary inside DashboardLayout so nav chrome survives - Status page KPIs showed fake zeros while the fetch failed -> em dash - Login lacked the forgot-password link (flow already existed end-to-end) - AdminSeedService: fresh DB had schema but no login possible; seeds super-admin + license from ADMIN_* env when users table is empty Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
2.5 KiB
Vue
106 lines
2.5 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch, onErrorCaptured } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import Icon from '@/components/ds/core/Icon.vue'
|
|
import Button from '@/components/ds/core/Button.vue'
|
|
|
|
withDefaults(defineProps<{
|
|
/** 'screen' fills the viewport (app root); 'content' fills its container (inside layout chrome) */
|
|
variant?: 'screen' | 'content'
|
|
}>(), { variant: 'screen' })
|
|
|
|
const route = useRoute()
|
|
const hasError = ref(false)
|
|
const errorMessage = ref('')
|
|
|
|
onErrorCaptured((err) => {
|
|
hasError.value = true
|
|
errorMessage.value = err.message || 'An unexpected error occurred'
|
|
console.error('ErrorBoundary caught:', err)
|
|
return false
|
|
})
|
|
|
|
// A failed view must not brick navigation — clear the error when the route changes
|
|
watch(() => route.fullPath, () => {
|
|
hasError.value = false
|
|
errorMessage.value = ''
|
|
})
|
|
|
|
function retry() {
|
|
hasError.value = false
|
|
errorMessage.value = ''
|
|
window.location.reload()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="hasError" class="eb-screen" :class="{ 'eb-screen--content': variant === 'content' }">
|
|
<div class="eb-card">
|
|
<div class="eb-icon-wrap">
|
|
<Icon name="triangle-alert" :size="24" :stroke-width="1.75" />
|
|
</div>
|
|
<h1 class="eb-title">Something went wrong</h1>
|
|
<p class="eb-msg">{{ errorMessage }}</p>
|
|
<Button icon="refresh-cw" @click="retry">Retry</Button>
|
|
</div>
|
|
</div>
|
|
<slot v-else />
|
|
</template>
|
|
|
|
<style scoped>
|
|
.eb-screen {
|
|
min-height: 100vh;
|
|
background: var(--surface-canvas);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-6);
|
|
}
|
|
|
|
.eb-screen--content {
|
|
min-height: 60vh;
|
|
background: transparent;
|
|
}
|
|
|
|
.eb-card {
|
|
background: var(--surface-base);
|
|
box-shadow: var(--ring-default), var(--shadow-md);
|
|
border-radius: var(--radius-xl);
|
|
padding: var(--space-8);
|
|
max-width: 380px;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-4);
|
|
text-align: center;
|
|
}
|
|
|
|
.eb-icon-wrap {
|
|
width: 52px;
|
|
height: 52px;
|
|
border-radius: var(--radius-lg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--status-offline-soft);
|
|
box-shadow: inset 0 0 0 1px var(--status-offline-border);
|
|
color: var(--status-offline);
|
|
flex: none;
|
|
}
|
|
|
|
.eb-title {
|
|
font-size: var(--text-xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.eb-msg {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-tertiary);
|
|
line-height: 1.55;
|
|
max-width: 300px;
|
|
}
|
|
</style>
|