feat: impl nonworking /status route
This commit is contained in:
parent
065b31a1ff
commit
cb8d1a801d
4 changed files with 386 additions and 3 deletions
140
app/components/ServiceCard.vue
Normal file
140
app/components/ServiceCard.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
<template>
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="service-info">
|
||||||
|
<h3 class="service-name">{{ name }}</h3>
|
||||||
|
<span class="info-tooltip" title="Click for historical uptime metrics">?</span>
|
||||||
|
</div>
|
||||||
|
<div class="current-status-dot" :class="status.toLowerCase()"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uptime-graph">
|
||||||
|
<div
|
||||||
|
v-for="(day, index) in uptimeHistory"
|
||||||
|
:key="index"
|
||||||
|
class="bar"
|
||||||
|
:class="day.status"
|
||||||
|
:title="formatTooltip(day)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="time-ago">90 days ago</span>
|
||||||
|
<span class="uptime-percentage">{{ uptimePercentage }}% uptime</span>
|
||||||
|
<span class="time-today">Today</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-label">
|
||||||
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: { type: String, required: true },
|
||||||
|
status: { type: String, default: 'Normal' },
|
||||||
|
uptimePercentage: { type: Number, default: 100.00 },
|
||||||
|
uptimeHistory: { type: Array, required: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatTooltip = (day) => {
|
||||||
|
return `${day.date}: ${day.status.toUpperCase()} (${day.incident || 'No incidents'})`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.service-card {
|
||||||
|
background-color: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: #f0f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-tooltip {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #8b949e;
|
||||||
|
background-color: #21262d;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-status-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.current-status-dot.normal { background-color: #238636; }
|
||||||
|
.current-status-dot.degraded { background-color: #d29922; }
|
||||||
|
.current-status-dot.outage { background-color: #f85149; }
|
||||||
|
|
||||||
|
.uptime-graph {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
height: 34px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #238636;
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scaleY(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar.normal { background-color: #238636; }
|
||||||
|
.bar.degraded { background-color: #d29922; }
|
||||||
|
.bar.outage { background-color: #f85149; }
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #8b949e;
|
||||||
|
border-bottom: 1px solid #21262d;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uptime-percentage {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
app/components/StatusHeader.vue
Normal file
90
app/components/StatusHeader.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<header class="status-header">
|
||||||
|
<div class="status-banner operational">
|
||||||
|
<span class="status-icon">✓</span>
|
||||||
|
All Systems Operational
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-header {
|
||||||
|
background-color: #0d1117;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-illustration {
|
||||||
|
height: 220px;
|
||||||
|
background: linear-gradient(180deg, #070a0e 0%, #0d1117 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factory-backdrop {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline {
|
||||||
|
position: absolute;
|
||||||
|
top: 60%;
|
||||||
|
left: 5%;
|
||||||
|
right: 5%;
|
||||||
|
height: 8px;
|
||||||
|
background: #21262d;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engineer {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-float-1 { top: 30%; left: 15%; animation: float 4s ease-in-out infinite; }
|
||||||
|
.dynamic-float-2 { top: 45%; left: 50%; animation: float 5s ease-in-out infinite 1s; }
|
||||||
|
.dynamic-float-3 { top: 25%; left: 80%; animation: float 6s ease-in-out infinite 0.5s; }
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: -25px auto 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner.operational {
|
||||||
|
background-color: #238636;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #2ea44f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="layout-wrapper">
|
||||||
<TheHeader />
|
<StatusHeader />
|
||||||
<slot /> <MainFooter />
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import StatusHeader from '~/components/StatusHeader.vue'
|
||||||
|
</script>
|
||||||
147
app/pages/status.vue
Normal file
147
app/pages/status.vue
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<div class="status-dashboard-container">
|
||||||
|
<div class="dashboard-meta">
|
||||||
|
<h2 class="title">Current Status</h2>
|
||||||
|
<div class="historical-link">
|
||||||
|
Uptime over the past 90 days. <a href="#">View historical uptime.</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-banner operational">
|
||||||
|
<svg class="octicon octicon-check" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" fill="currentColor">
|
||||||
|
<path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm4.78 6.28-5.5 5.5a.75.75 0 0 1-1.06 0l-2.5-2.5a.751.751 0 0 1 .018-1.08.75.75 0 0 1 1.042.02L11 13.44l4.97-4.97a.75.75 0 0 1 1.06 1.06Z"></path>
|
||||||
|
</svg>
|
||||||
|
All Systems Operational
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="services-grid">
|
||||||
|
<ServiceCard
|
||||||
|
v-for="service in mockServices"
|
||||||
|
:key="service.id"
|
||||||
|
:name="service.name"
|
||||||
|
:status="service.status"
|
||||||
|
:uptime-percentage="service.uptimePercentage"
|
||||||
|
:uptime-history="service.history"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ServiceCard from '~/components/ServiceCard.vue'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
const sharedHistory = (() => {
|
||||||
|
const history = []
|
||||||
|
const now = new Date()
|
||||||
|
for (let i = 89; i >= 0; i--) {
|
||||||
|
const dayDate = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
||||||
|
history.push({
|
||||||
|
date: dayDate.toISOString().split('T')[0],
|
||||||
|
status: 'normal',
|
||||||
|
incident: 'No incidents'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return history
|
||||||
|
})()
|
||||||
|
|
||||||
|
const serviceNames = [
|
||||||
|
'Website', 'Account', 'Connection Test', 'Super Mario Maker',
|
||||||
|
'Mario Tennis: Ultra Smash', 'Wii Sports Club', 'Splatoon Global Testfire',
|
||||||
|
'Splatoon', 'Friends', 'ぷよぷよテトリス', 'Wii U Chat',
|
||||||
|
'Sonic & All-Stars Racing Transformed', 'Minecraft: Wii U Edition', 'Fast Racing Neo'
|
||||||
|
]
|
||||||
|
|
||||||
|
//only here until i actually implement health checks, its 6:30am and i havent slept..
|
||||||
|
const mockServices = ref(
|
||||||
|
serviceNames.map((name, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name,
|
||||||
|
status: 'Normal',
|
||||||
|
uptimePercentage: 100.00,
|
||||||
|
history: sharedHistory
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||||
|
|
||||||
|
.status-dashboard-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: 'Archivo Black', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #f0f6fc;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-link {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-link a {
|
||||||
|
color: #f0883e;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner.operational {
|
||||||
|
background-color: #238636;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #2ea44f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.octicon-check {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.dashboard-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue