Page MenuHomeDevCentral

D3980.diff
No OneTemporary

D3980.diff

diff --git a/frontend/src/components/__tests__/router.test.js b/frontend/src/components/__tests__/router.test.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/__tests__/router.test.js
@@ -0,0 +1,59 @@
+import { describe, it, expect, vi } from 'vitest'
+import { createRouter, createWebHistory } from 'vue-router'
+
+vi.mock('@/plugins/api', () => ({
+ authApi: { verify: vi.fn() },
+}))
+
+const routes = [
+ {
+ path: '/',
+ name: 'status',
+ component: { template: '<div>Status</div>' },
+ },
+ {
+ path: '/incidents',
+ name: 'incident-history',
+ component: { template: '<div>Incident History</div>' },
+ },
+]
+
+function createTestRouter() {
+ return createRouter({
+ history: createWebHistory(),
+ routes,
+ })
+}
+
+describe('Router', () => {
+ it('/incidents resolves to incident-history', async () => {
+ const router = createTestRouter()
+ await router.push('/incidents')
+ await router.isReady()
+
+ expect(router.currentRoute.value.name).toBe('incident-history')
+ expect(router.currentRoute.value.path).toBe('/incidents')
+ })
+
+ it('navigates from / to /incidents', async () => {
+ const router = createTestRouter()
+ await router.push('/')
+ await router.isReady()
+
+ expect(router.currentRoute.value.name).toBe('status')
+
+ await router.push('/incidents')
+
+ expect(router.currentRoute.value.name).toBe('incident-history')
+ })
+
+ it('navigates from /incidents back to /', async () => {
+ const router = createTestRouter()
+ await router.push('/incidents')
+ await router.isReady()
+
+ await router.push('/')
+
+ expect(router.currentRoute.value.name).toBe('status')
+ })
+})
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -10,6 +10,11 @@
name: 'status',
component: StatusPage,
},
+ {
+ path: '/incidents',
+ name: 'incident-history',
+ component: () => import('@/views/IncidentHistory.vue'),
+ },
{
path: '/confirm/:token',
name: 'confirm-subscription',
diff --git a/frontend/src/views/IncidentHistory.vue b/frontend/src/views/IncidentHistory.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/views/IncidentHistory.vue
@@ -0,0 +1,112 @@
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useIncidents } from '@/composables/useIncidents'
+import { incidentsApi } from '@/plugins/api'
+import IncidentTimeline from '@/components/IncidentTimeline.vue'
+
+const FILTER_OPTIONS = [
+ { label: 'Last 7 Days', value: 7 },
+ { label: 'Last 30 Days', value: 30 },
+ { label: 'Last 90 Days', value: 90 },
+ { label: 'All', value: 'all' },
+]
+
+const { incidents, loading, fetchIncidents } = useIncidents()
+const activeFilter = ref(7)
+const detailedIncidents = ref([])
+const detailsLoading = ref(false)
+
+async function fetchDetailedIncidents() {
+ detailsLoading.value = true
+ try {
+ detailedIncidents.value = await Promise.all(
+ incidents.value.map(
+ (incident) => incidentsApi.getById(incident.id).catch(() => incident)
+ )
+ )
+ } catch (err) {
+ console.error('Failed to fetch incident details:', err)
+ } finally {
+ detailsLoading.value = false
+ }
+}
+
+function isWithinDays(dateString, days) {
+ const cutoffDate = new Date()
+ cutoffDate.setDate(cutoffDate.getDate() - days)
+
+ return new Date(dateString) >= cutoffDate
+}
+
+function filterButtonClass(value) {
+ if (activeFilter.value === value) {
+ return 'bg-brand-500 text-white'
+ }
+
+ return 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
+}
+
+const filteredIncidents = computed(() => {
+ if (activeFilter.value === 'all') {
+ return detailedIncidents.value
+ }
+
+ return detailedIncidents.value.filter((incident) => {
+ return isWithinDays(incident.created_at, activeFilter.value)
+ })
+})
+
+const isLoading = computed(() => loading.value || detailsLoading.value)
+
+onMounted(async () => {
+ await fetchIncidents()
+ await fetchDetailedIncidents()
+})
+</script>
+
+<template>
+ <div class="max-w-3xl mx-auto px-4 sm:px-6 py-8 space-y-6">
+ <div class="flex items-center justify-between mb-2">
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
+ Incident History
+ </h1>
+ <router-link
+ to="/"
+ class="text-sm text-brand-500 hover:text-brand-600"
+ >
+ &larr; Back to status
+ </router-link>
+ </div>
+
+ <div class="flex gap-2">
+ <button
+ v-for="filter in FILTER_OPTIONS"
+ :key="filter.value"
+ :class="filterButtonClass(filter.value)"
+ class="px-4 py-2 rounded-md text-sm font-medium transition-colors"
+ @click="activeFilter = filter.value"
+ >
+ {{ filter.label }}
+ </button>
+ </div>
+
+ <!-- Loading -->
+ <div v-if="isLoading" class="flex justify-center py-20">
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-brand-500"></div>
+ </div>
+
+ <template v-else>
+ <div v-if="filteredIncidents.length === 0" class="card p-6 text-center text-gray-500">
+ No incidents reported in this timeframe.
+ </div>
+
+ <div v-else class="space-y-3">
+ <IncidentTimeline
+ v-for="incident in filteredIncidents"
+ :key="incident.id"
+ :incident="incident"
+ />
+ </div>
+ </template>
+ </div>
+</template>

File Metadata

Mime Type
text/plain
Expires
Sun, Mar 1, 18:33 (18 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3483013
Default Alt Text
D3980.diff (5 KB)

Event Timeline