5128e1551c
Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
than string formatting: the password contains '@', which would otherwise be
parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
no tables are created, at import time or otherwise
Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000
The database paper_doc was created in TiDB out of band; no table exists yet.
101 lines
2.6 KiB
Vue
101 lines
2.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { Refresh } from '@element-plus/icons-vue'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
import { fetchHealth, type HealthResponse } from '@/api/health'
|
|
import { useAppStore } from '@/stores/app'
|
|
|
|
const appStore = useAppStore()
|
|
|
|
const health = ref<HealthResponse | null>(null)
|
|
const loading = ref(false)
|
|
|
|
const databaseTagType = computed(() => (health.value?.database === 'ok' ? 'success' : 'danger'))
|
|
|
|
async function loadHealth(): Promise<void> {
|
|
loading.value = true
|
|
try {
|
|
health.value = await fetchHealth()
|
|
appStore.markChecked()
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
ElMessage.error(message)
|
|
health.value = null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadHealth)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="home">
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>Backend connectivity</span>
|
|
<el-button :icon="Refresh" :loading="loading" @click="loadHealth">Re-check</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<el-descriptions v-if="health" :column="1" border>
|
|
<el-descriptions-item label="API">
|
|
{{ health.app }}
|
|
<el-tag type="success" size="small">{{ health.status }}</el-tag>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="TiDB">
|
|
<el-tag :type="databaseTagType" size="small">{{ health.database }}</el-tag>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="Target">
|
|
<code>{{ health.database_target }}</code>
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<el-empty v-else description="No response from the API yet" />
|
|
</el-card>
|
|
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>Persisted state</span>
|
|
<el-switch
|
|
v-model="appStore.sidebarCollapsed"
|
|
active-text="Sidebar collapsed"
|
|
inline-prompt
|
|
/>
|
|
</div>
|
|
</template>
|
|
<p>
|
|
Last probe:
|
|
<strong>{{ appStore.lastCheckedAt ?? 'never' }}</strong>
|
|
</p>
|
|
<p class="hint">
|
|
Both values are stored in <code>localStorage</code> under
|
|
<code>paper-doc:app</code>. Reload the page — they survive.
|
|
</p>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.home {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.hint {
|
|
color: var(--el-text-color-secondary);
|
|
font-size: 13px;
|
|
margin: 0;
|
|
}
|
|
</style>
|