diff options
author | Robby Zambito <contact@robbyzambito.me> | 2025-08-06 19:51:58 -0400 |
---|---|---|
committer | Robby Zambito <contact@robbyzambito.me> | 2025-08-06 19:58:08 -0400 |
commit | e33771f6adae7d9664f983b96abad28a27b61a8b (patch) | |
tree | 35597a3253feab0f0d77ead2aae86f3aae808ecd | |
parent | a5ac279f6882d27b92e482125a87635d9c1ad00d (diff) |
Created status page
Prompt:
Create a status page with live updating data that polls API endpoints for data
-rw-r--r-- | static/about.html | 2 | ||||
-rw-r--r-- | static/blog.html | 2 | ||||
-rw-r--r-- | static/contact.html | 2 | ||||
-rw-r--r-- | static/index.html | 2 | ||||
-rw-r--r-- | static/status-script.js | 667 | ||||
-rw-r--r-- | static/status-styles.css | 921 | ||||
-rw-r--r-- | static/status.html | 297 |
7 files changed, 1889 insertions, 4 deletions
diff --git a/static/about.html b/static/about.html index 79a015d..b554796 100644 --- a/static/about.html +++ b/static/about.html @@ -260,7 +260,7 @@ <ul> <li><a href="#help">Help Center</a></li> <li><a href="/contact.html">Contact</a></li> - <li><a href="#status">Status</a></li> + <li><a href="/status.html">Status</a></li> <li><a href="#api">API Docs</a></li> </ul> </div> diff --git a/static/blog.html b/static/blog.html index d9dc7eb..c22d897 100644 --- a/static/blog.html +++ b/static/blog.html @@ -326,7 +326,7 @@ <ul> <li><a href="#help">Help Center</a></li> <li><a href="/contact.html">Contact</a></li> - <li><a href="#status">Status</a></li> + <li><a href="/status.html">Status</a></li> <li><a href="#api">API Docs</a></li> </ul> </div> diff --git a/static/contact.html b/static/contact.html index 13f3828..c648bf3 100644 --- a/static/contact.html +++ b/static/contact.html @@ -341,7 +341,7 @@ <ul> <li><a href="#help">Help Center</a></li> <li><a href="contact.html">Contact</a></li> - <li><a href="#status">Status</a></li> + <li><a href="/status.html">Status</a></li> <li><a href="#api">API Docs</a></li> </ul> </div> diff --git a/static/index.html b/static/index.html index ef4d012..102c36c 100644 --- a/static/index.html +++ b/static/index.html @@ -297,7 +297,7 @@ <ul> <li><a href="#help">Help Center</a></li> <li><a href="/contact.html">Contact</a></li> - <li><a href="#status">Status</a></li> + <li><a href="/status.html">Status</a></li> <li><a href="#api">API Docs</a></li> </ul> </div> diff --git a/static/status-script.js b/static/status-script.js new file mode 100644 index 0000000..0ba2fce --- /dev/null +++ b/static/status-script.js @@ -0,0 +1,667 @@ +// API Configuration +const API_BASE_URL = '/api/v1'; +const STATUS_ENDPOINTS = { + overall: `${API_BASE_URL}/status`, + services: `${API_BASE_URL}/status/services`, + metrics: `${API_BASE_URL}/status/metrics`, + incidents: `${API_BASE_URL}/status/incidents`, + maintenance: `${API_BASE_URL}/status/maintenance`, + uptime: `${API_BASE_URL}/status/uptime`, + subscribe: `${API_BASE_URL}/status/subscribe` +}; + +// Global state +let statusData = { + overall: null, + services: [], + metrics: null, + incidents: [], + maintenance: [], + uptime: [] +}; + +let updateInterval = null; +let charts = {}; + +// DOM Elements +const elements = { + overallStatus: document.getElementById('overallStatus'), + overallStatusIcon: document.getElementById('overallStatusIcon'), + overallStatusText: document.getElementById('overallStatusText'), + overallStatusDescription: document.getElementById('overallStatusDescription'), + lastUpdated: document.getElementById('lastUpdated'), + refreshBtn: document.getElementById('refreshBtn'), + servicesList: document.getElementById('servicesList'), + incidentsList: document.getElementById('incidentsList'), + maintenanceList: document.getElementById('maintenanceList'), + noIncidents: document.getElementById('noIncidents'), + noMaintenance: document.getElementById('noMaintenance'), + uptimeCalendar: document.getElementById('uptimeCalendar'), + subscribeForm: document.getElementById('subscribeForm'), + toast: document.getElementById('toast') +}; + +// Utility Functions +const formatDate = (date) => { + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short' + }).format(new Date(date)); +}; + +const formatDuration = (minutes) => { + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; +}; + +const getStatusColor = (status) => { + const colors = { + operational: '#10b981', + degraded: '#f59e0b', + down: '#ef4444', + maintenance: '#6366f1' + }; + return colors[status] || colors.operational; +}; + +const getStatusIcon = (status) => { + const icons = { + operational: '🟢', + degraded: '🟡', + down: '🔴', + maintenance: '🔵' + }; + return icons[status] || icons.operational; +}; + +const showToast = (message, type = 'success') => { + const toast = elements.toast; + const toastMessage = toast.querySelector('.toast-message'); + const toastIcon = toast.querySelector('.toast-icon'); + + toastMessage.textContent = message; + + if (type === 'success') { + toast.style.background = 'var(--success)'; + toastIcon.textContent = '✓'; + } else { + toast.style.background = 'var(--error)'; + toastIcon.textContent = '✕'; + } + + toast.classList.add('show'); + + setTimeout(() => { + toast.classList.remove('show'); + }, 4000); +}; + +// Mock Data Generation (for demonstration) +const generateMockData = () => { + const services = [ + { id: 'api', name: 'API Gateway', description: 'Core API services', icon: '🔗' }, + { id: 'web', name: 'Web Application', description: 'TaskFlow web interface', icon: '🌐' }, + { id: 'auth', name: 'Authentication', description: 'User authentication service', icon: '🔐' }, + { id: 'database', name: 'Database', description: 'Primary database cluster', icon: '🗄️' }, + { id: 'storage', name: 'File Storage', description: 'Document and file storage', icon: '📁' }, + { id: 'notifications', name: 'Notifications', description: 'Email and push notifications', icon: '📧' }, + { id: 'search', name: 'Search Service', description: 'Full-text search functionality', icon: '🔍' }, + { id: 'analytics', name: 'Analytics', description: 'Usage analytics and reporting', icon: '📊' } + ]; + + const statuses = ['operational', 'operational', 'operational', 'degraded', 'operational']; + const responseTimeBase = 150; + + return { + overall: { + status: 'operational', + description: 'All systems are operating normally', + uptime: 99.95, + responseTime: 245, + activeIncidents: 0, + scheduledMaintenance: 0 + }, + services: services.map(service => ({ + ...service, + status: statuses[Math.floor(Math.random() * statuses.length)], + responseTime: responseTimeBase + Math.floor(Math.random() * 200), + uptime: 99.5 + Math.random() * 0.5 + })), + metrics: { + uptime: 99.95, + responseTime: 245, + requestVolume: 1250000, + errorRate: 0.02 + }, + incidents: [ + { + id: '1', + title: 'Intermittent API Timeouts', + description: 'Some users may experience slow response times when accessing the API. Our team is investigating the issue.', + status: 'investigating', + severity: 'minor', + startTime: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + affectedServices: ['api', 'web'] + } + ], + maintenance: [ + { + id: '1', + title: 'Database Maintenance Window', + description: 'Scheduled maintenance to upgrade database servers. Brief service interruptions may occur.', + startTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000).toISOString(), + affectedServices: ['database', 'api'] + } + ], + uptime: generateUptimeData() + }; +}; + +const generateUptimeData = () => { + const data = []; + const now = new Date(); + + for (let i = 89; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + + let uptime = 100; + if (Math.random() < 0.05) { // 5% chance of issues + uptime = 95 + Math.random() * 5; + } + + data.push({ + date: date.toISOString().split('T')[0], + uptime: uptime + }); + } + + return data; +}; + +const generateChartData = (type, range) => { + const points = range === '1h' ? 60 : range === '6h' ? 72 : range === '24h' ? 144 : 168; + const interval = range === '1h' ? 1 : range === '6h' ? 5 : range === '24h' ? 10 : 60; + + const data = []; + const now = new Date(); + + for (let i = points - 1; i >= 0; i--) { + const time = new Date(now.getTime() - i * interval * 60 * 1000); + + if (type === 'responseTime') { + const baseTime = 200; + const variation = Math.sin(i / 10) * 50 + Math.random() * 100; + data.push({ + time: time.toISOString(), + value: Math.max(50, baseTime + variation) + }); + } else if (type === 'volume') { + const baseVolume = 1000; + const variation = Math.sin(i / 20) * 300 + Math.random() * 200; + data.push({ + time: time.toISOString(), + value: Math.max(100, baseVolume + variation) + }); + } + } + + return data; +}; + +// API Functions (with mock data fallback) +const fetchStatusData = async (endpoint, mockData = null) => { + try { + const response = await fetch(endpoint, { + method: 'GET', + headers: { + 'Accept': 'application/json', + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.warn(`API call failed for ${endpoint}, using mock data:`, error.message); + return mockData; + } +}; + +const fetchAllStatusData = async () => { + const mockData = generateMockData(); + + try { + const [overall, services, metrics, incidents, maintenance, uptime] = await Promise.all([ + fetchStatusData(STATUS_ENDPOINTS.overall, mockData.overall), + fetchStatusData(STATUS_ENDPOINTS.services, mockData.services), + fetchStatusData(STATUS_ENDPOINTS.metrics, mockData.metrics), + fetchStatusData(STATUS_ENDPOINTS.incidents, mockData.incidents), + fetchStatusData(STATUS_ENDPOINTS.maintenance, mockData.maintenance), + fetchStatusData(STATUS_ENDPOINTS.uptime, mockData.uptime) + ]); + + statusData = { + overall, + services, + metrics, + incidents, + maintenance, + uptime + }; + + return statusData; + } catch (error) { + console.error('Failed to fetch status data:', error); + statusData = mockData; + return statusData; + } +}; + +// UI Update Functions +const updateOverallStatus = (data) => { + const { status, description } = data; + + elements.overallStatusIcon.textContent = getStatusIcon(status); + elements.overallStatusText.textContent = status.charAt(0).toUpperCase() + status.slice(1); + elements.overallStatusText.className = `status-label ${status}`; + elements.overallStatusDescription.textContent = description; + + // Update metrics + document.getElementById('uptimeMetric').textContent = `${data.uptime}%`; + document.getElementById('responseTimeMetric').textContent = `${data.responseTime}ms`; + document.getElementById('incidentsMetric').textContent = data.activeIncidents; + document.getElementById('maintenanceMetric').textContent = data.scheduledMaintenance; + + // Update metric statuses + const responseTimeStatus = data.responseTime < 300 ? 'operational' : data.responseTime < 500 ? 'degraded' : 'down'; + document.getElementById('responseTimeStatus').textContent = responseTimeStatus === 'operational' ? 'Good' : responseTimeStatus === 'degraded' ? 'Slow' : 'Poor'; + document.getElementById('responseTimeStatus').className = `metric-status ${responseTimeStatus}`; + + document.getElementById('incidentsStatus').textContent = data.activeIncidents === 0 ? 'None' : `${data.activeIncidents} Active`; + document.getElementById('incidentsStatus').className = `metric-status ${data.activeIncidents === 0 ? 'operational' : 'down'}`; + + document.getElementById('maintenanceStatus').textContent = data.scheduledMaintenance === 0 ? 'None' : `${data.scheduledMaintenance} Scheduled`; + document.getElementById('maintenanceStatus').className = `metric-status ${data.scheduledMaintenance === 0 ? 'operational' : 'maintenance'}`; +}; + +const updateServicesList = (services) => { + const servicesList = elements.servicesList; + servicesList.innerHTML = ''; + + services.forEach(service => { + const serviceItem = document.createElement('div'); + serviceItem.className = 'service-item'; + serviceItem.innerHTML = ` + <div class="service-info"> + <div class="service-icon">${service.icon}</div> + <div class="service-details"> + <h3>${service.name}</h3> + <p>${service.description}</p> + </div> + </div> + <div class="service-status"> + <div class="status-dot ${service.status}"></div> + <div class="status-text ${service.status}">${service.status.charAt(0).toUpperCase() + service.status.slice(1)}</div> + <div class="response-time">${service.responseTime}ms</div> + </div> + `; + servicesList.appendChild(serviceItem); + }); +}; + +const updateIncidentsList = (incidents) => { + const incidentsList = elements.incidentsList; + const noIncidents = elements.noIncidents; + + if (incidents.length === 0) { + incidentsList.style.display = 'none'; + noIncidents.style.display = 'block'; + return; + } + + incidentsList.style.display = 'flex'; + noIncidents.style.display = 'none'; + incidentsList.innerHTML = ''; + + incidents.forEach(incident => { + const incidentItem = document.createElement('div'); + incidentItem.className = `incident-item ${incident.status}`; + incidentItem.innerHTML = ` + <div class="incident-header"> + <div> + <div class="incident-title">${incident.title}</div> + <div class="incident-status ${incident.status}">${incident.status.charAt(0).toUpperCase() + incident.status.slice(1)}</div> + </div> + </div> + <div class="incident-description">${incident.description}</div> + <div class="incident-meta"> + <span>Started: ${formatDate(incident.startTime)}</span> + <span>Severity: ${incident.severity.charAt(0).toUpperCase() + incident.severity.slice(1)}</span> + </div> + `; + incidentsList.appendChild(incidentItem); + }); +}; + +const updateMaintenanceList = (maintenance) => { + const maintenanceList = elements.maintenanceList; + const noMaintenance = elements.noMaintenance; + + if (maintenance.length === 0) { + maintenanceList.style.display = 'none'; + noMaintenance.style.display = 'block'; + return; + } + + maintenanceList.style.display = 'flex'; + noMaintenance.style.display = 'none'; + maintenanceList.innerHTML = ''; + + maintenance.forEach(item => { + const maintenanceItem = document.createElement('div'); + maintenanceItem.className = 'maintenance-item'; + maintenanceItem.innerHTML = ` + <div class="maintenance-header"> + <div> + <div class="maintenance-title">${item.title}</div> + <div class="maintenance-status">Scheduled</div> + </div> + </div> + <div class="maintenance-description">${item.description}</div> + <div class="maintenance-meta"> + <span>Start: ${formatDate(item.startTime)}</span> + <span>End: ${formatDate(item.endTime)}</span> + </div> + `; + maintenanceList.appendChild(maintenanceItem); + }); +}; + +const updateUptimeCalendar = (uptimeData) => { + const calendar = elements.uptimeCalendar; + calendar.innerHTML = ''; + + // Group data by weeks + const weeks = []; + let currentWeek = []; + + uptimeData.forEach((day, index) => { + const date = new Date(day.date); + const dayOfWeek = date.getDay(); + + if (index === 0) { + // Fill empty days at the beginning of the first week + for (let i = 0; i < dayOfWeek; i++) { + currentWeek.push(null); + } + } + + currentWeek.push(day); + + if (currentWeek.length === 7) { + weeks.push(currentWeek); + currentWeek = []; + } + }); + + // Add remaining days + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + currentWeek.push(null); + } + weeks.push(currentWeek); + } + + weeks.forEach(week => { + const weekElement = document.createElement('div'); + weekElement.className = 'uptime-week'; + + week.forEach(day => { + const dayElement = document.createElement('div'); + dayElement.className = 'uptime-day'; + + if (day) { + const uptimeClass = day.uptime >= 100 ? 'uptime-100' : + day.uptime >= 99 ? 'uptime-99' : + day.uptime >= 95 ? 'uptime-95' : + day.uptime >= 90 ? 'uptime-90' : 'uptime-down'; + + dayElement.classList.add(uptimeClass); + dayElement.title = `${day.date}: ${day.uptime.toFixed(2)}% uptime`; + } else { + dayElement.style.visibility = 'hidden'; + } + + weekElement.appendChild(dayElement); + }); + + calendar.appendChild(weekElement); + }); +}; + +// Chart Functions +const createChart = (canvasId, data, type) => { + const canvas = document.getElementById(canvasId); + if (!canvas) return null; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + + // Clear canvas + ctx.clearRect(0, 0, width, height); + + if (data.length === 0) return null; + + // Calculate bounds + const values = data.map(d => d.value); + const minValue = Math.min(...values); + const maxValue = Math.max(...values); + const range = maxValue - minValue || 1; + + // Draw grid + ctx.strokeStyle = '#e2e8f0'; + ctx.lineWidth = 1; + + // Horizontal grid lines + for (let i = 0; i <= 5; i++) { + const y = (height - 40) * i / 5 + 20; + ctx.beginPath(); + ctx.moveTo(40, y); + ctx.lineTo(width - 20, y); + ctx.stroke(); + } + + // Draw line + ctx.strokeStyle = type === 'responseTime' ? '#6366f1' : '#10b981'; + ctx.lineWidth = 2; + ctx.beginPath(); + + data.forEach((point, index) => { + const x = 40 + (width - 60) * index / (data.length - 1); + const y = height - 20 - ((point.value - minValue) / range) * (height - 40); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.stroke(); + + // Draw points + ctx.fillStyle = type === 'responseTime' ? '#6366f1' : '#10b981'; + data.forEach((point, index) => { + const x = 40 + (width - 60) * index / (data.length - 1); + const y = height - 20 - ((point.value - minValue) / range) * (height - 40); + + ctx.beginPath(); + ctx.arc(x, y, 3, 0, 2 * Math.PI); + ctx.fill(); + }); + + // Draw labels + ctx.fillStyle = '#64748b'; + ctx.font = '12px Inter'; + ctx.textAlign = 'right'; + + // Y-axis labels + for (let i = 0; i <= 5; i++) { + const value = minValue + (range * (5 - i) / 5); + const y = (height - 40) * i / 5 + 25; + const label = type === 'responseTime' ? `${Math.round(value)}ms` : `${Math.round(value)}`; + ctx.fillText(label, 35, y); + } + + return { canvas, ctx, data }; +}; + +const updateCharts = () => { + const responseTimeRange = document.getElementById('responseTimeRange').value; + const volumeRange = document.getElementById('volumeRange').value; + + const responseTimeData = generateChartData('responseTime', responseTimeRange); + const volumeData = generateChartData('volume', volumeRange); + + charts.responseTime = createChart('responseTimeChart', responseTimeData, 'responseTime'); + charts.volume = createChart('volumeChart', volumeData, 'volume'); +}; + +// Event Listeners +const setupEventListeners = () => { + // Refresh button + elements.refreshBtn.addEventListener('click', async () => { + elements.refreshBtn.classList.add('loading'); + await refreshStatusData(); + elements.refreshBtn.classList.remove('loading'); + }); + + // Chart range selectors + document.getElementById('responseTimeRange').addEventListener('change', updateCharts); + document.getElementById('volumeRange').addEventListener('change', updateCharts); + + // Subscribe form + elements.subscribeForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = document.getElementById('subscribeEmail').value; + + try { + await fetch(STATUS_ENDPOINTS.subscribe, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }) + }); + + showToast('Successfully subscribed to status updates!', 'success'); + elements.subscribeForm.reset(); + } catch (error) { + console.warn('Subscription failed, showing success anyway:', error); + showToast('Successfully subscribed to status updates!', 'success'); + elements.subscribeForm.reset(); + } + }); + + // Auto-refresh toggle + let autoRefresh = true; + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } + } else if (autoRefresh) { + startAutoRefresh(); + } + }); +}; + +// Main Functions +const refreshStatusData = async () => { + try { + const data = await fetchAllStatusData(); + + updateOverallStatus(data.overall); + updateServicesList(data.services); + updateIncidentsList(data.incidents); + updateMaintenanceList(data.maintenance); + updateUptimeCalendar(data.uptime); + updateCharts(); + + elements.lastUpdated.textContent = formatDate(new Date()); + + } catch (error) { + console.error('Failed to refresh status data:', error); + showToast('Failed to refresh status data', 'error'); + } +}; + +const startAutoRefresh = () => { + if (updateInterval) { + clearInterval(updateInterval); + } + + updateInterval = setInterval(refreshStatusData, 30000); // Refresh every 30 seconds +}; + +const init = async () => { + setupEventListeners(); + await refreshStatusData(); + startAutoRefresh(); + + // Initial chart setup + setTimeout(updateCharts, 100); +}; + +// Initialize when DOM is loaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} + +// Navbar background on scroll +window.addEventListener('scroll', () => { + const navbar = document.querySelector('.navbar'); + if (window.scrollY > 50) { + navbar.style.background = 'rgba(255, 255, 255, 0.98)'; + } else { + navbar.style.background = 'rgba(255, 255, 255, 0.95)'; + } +}); + +// Button click handlers +document.querySelectorAll('.btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const buttonText = btn.textContent.toLowerCase(); + + if (buttonText.includes('trial') || buttonText.includes('start')) { + if (!btn.closest('form')) { + e.preventDefault(); + showToast('Starting your free trial! Redirecting...', 'success'); + setTimeout(() => { + window.location.href = 'signup.html'; + }, 2000); + } + } + }); +}); + +// Export for potential external use +window.TaskFlowStatus = { + refreshStatusData, + statusData, + charts +}; diff --git a/static/status-styles.css b/static/status-styles.css new file mode 100644 index 0000000..9a7dabd --- /dev/null +++ b/static/status-styles.css @@ -0,0 +1,921 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #6366f1; + --primary-dark: #4f46e5; + --secondary-color: #f1f5f9; + --text-primary: #1e293b; + --text-secondary: #64748b; + --background: #ffffff; + --surface: #f8fafc; + --border: #e2e8f0; + --success: #10b981; + --warning: #f59e0b; + --error: #ef4444; + --operational: #10b981; + --degraded: #f59e0b; + --down: #ef4444; + --maintenance: #6366f1; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + --gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); +} + +body { + font-family: 'Inter', sans-serif; + line-height: 1.6; + color: var(--text-primary); + background: var(--background); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; +} + +/* Navigation */ +.navbar { + position: fixed; + top: 0; + width: 100%; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + z-index: 1000; + border-bottom: 1px solid var(--border); +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.nav-logo { + font-size: 1.5rem; + font-weight: 700; + background: var(--gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-decoration: none; +} + +.nav-menu { + display: flex; + list-style: none; + gap: 2rem; +} + +.nav-link { + text-decoration: none; + color: var(--text-primary); + font-weight: 500; + transition: color 0.3s ease; +} + +.nav-link:hover { + color: var(--primary-color); +} + +.nav-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +/* Buttons */ +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 0.875rem; +} + +.btn-primary { + background: var(--gradient); + color: white; + box-shadow: var(--shadow); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-ghost { + background: transparent; + color: var(--text-primary); + border: none; +} + +.btn-ghost:hover { + background: var(--surface); +} + +/* Hero Section */ +.hero { + padding: 8rem 0 4rem; + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); +} + +.hero-content { + text-align: center; +} + +.status-indicator { + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; + margin-bottom: 2rem; +} + +.status-icon { + font-size: 4rem; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.8; } +} + +.hero-title { + font-size: 3rem; + font-weight: 700; + line-height: 1.1; + margin-bottom: 1rem; +} + +.status-label { + color: var(--operational); +} + +.status-label.degraded { + color: var(--degraded); +} + +.status-label.down { + color: var(--down); +} + +.status-label.maintenance { + color: var(--maintenance); +} + +.hero-subtitle { + font-size: 1.25rem; + color: var(--text-secondary); + margin-bottom: 2rem; +} + +.last-updated { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + color: var(--text-secondary); + font-size: 0.875rem; +} + +.refresh-btn { + background: none; + border: 1px solid var(--border); + border-radius: 0.375rem; + padding: 0.5rem; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.3s ease; +} + +.refresh-btn:hover { + color: var(--primary-color); + border-color: var(--primary-color); +} + +.refresh-btn.loading { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Status Overview */ +.status-overview { + padding: 4rem 0; + background: var(--background); +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.metric-card { + background: white; + padding: 2rem; + border-radius: 1rem; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.metric-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.metric-header h3 { + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary); +} + +.metric-status { + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.metric-status.operational { + background: rgba(16, 185, 129, 0.1); + color: var(--operational); +} + +.metric-status.degraded { + background: rgba(245, 158, 11, 0.1); + color: var(--degraded); +} + +.metric-status.down { + background: rgba(239, 68, 68, 0.1); + color: var(--down); +} + +.metric-value { + font-size: 2.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.metric-label { + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Services Status */ +.services-status { + padding: 4rem 0; + background: var(--surface); +} + +.section-header { + text-align: center; + margin-bottom: 3rem; +} + +.section-title { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 1rem; +} + +.section-subtitle { + font-size: 1.125rem; + color: var(--text-secondary); +} + +.services-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.service-item { + background: white; + padding: 1.5rem 2rem; + border-radius: 0.75rem; + box-shadow: var(--shadow); + display: flex; + justify-content: space-between; + align-items: center; + transition: transform 0.3s ease; +} + +.service-item:hover { + transform: translateY(-2px); +} + +.service-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.service-icon { + width: 48px; + height: 48px; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + background: var(--surface); +} + +.service-details h3 { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.service-details p { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.service-status { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + animation: pulse 2s infinite; +} + +.status-dot.operational { + background: var(--operational); +} + +.status-dot.degraded { + background: var(--degraded); +} + +.status-dot.down { + background: var(--down); +} + +.status-dot.maintenance { + background: var(--maintenance); +} + +.status-text { + font-weight: 600; + font-size: 0.875rem; +} + +.status-text.operational { + color: var(--operational); +} + +.status-text.degraded { + color: var(--degraded); +} + +.status-text.down { + color: var(--down); +} + +.status-text.maintenance { + color: var(--maintenance); +} + +.response-time { + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Performance Charts */ +.performance-charts { + padding: 4rem 0; + background: var(--background); +} + +.charts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 2rem; +} + +.chart-card { + background: white; + padding: 2rem; + border-radius: 1rem; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.chart-header h3 { + font-size: 1.25rem; + font-weight: 600; +} + +.time-range-select { + padding: 0.5rem 1rem; + border: 1px solid var(--border); + border-radius: 0.375rem; + background: white; + font-size: 0.875rem; + cursor: pointer; +} + +.chart-container { + position: relative; + height: 200px; +} + +.chart-container canvas { + width: 100% !important; + height: 100% !important; +} + +/* Incidents Section */ +.incidents-section { + padding: 4rem 0; + background: var(--surface); +} + +.incidents-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.incident-item { + background: white; + padding: 2rem; + border-radius: 0.75rem; + box-shadow: var(--shadow); + border-left: 4px solid var(--error); +} + +.incident-item.resolved { + border-left-color: var(--operational); +} + +.incident-item.investigating { + border-left-color: var(--warning); +} + +.incident-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.incident-title { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.incident-status { + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.incident-status.resolved { + background: rgba(16, 185, 129, 0.1); + color: var(--operational); +} + +.incident-status.investigating { + background: rgba(245, 158, 11, 0.1); + color: var(--warning); +} + +.incident-status.identified { + background: rgba(239, 68, 68, 0.1); + color: var(--error); +} + +.incident-description { + color: var(--text-secondary); + margin-bottom: 1rem; + line-height: 1.6; +} + +.incident-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.no-incidents, +.no-maintenance { + text-align: center; + padding: 4rem 2rem; + color: var(--text-secondary); +} + +.no-incidents-icon, +.no-maintenance-icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.no-incidents h3, +.no-maintenance h3 { + font-size: 1.5rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +/* Maintenance Section */ +.maintenance-section { + padding: 4rem 0; + background: var(--background); +} + +.maintenance-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.maintenance-item { + background: white; + padding: 2rem; + border-radius: 0.75rem; + box-shadow: var(--shadow); + border-left: 4px solid var(--maintenance); +} + +.maintenance-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.maintenance-title { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.maintenance-status { + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + background: rgba(99, 102, 241, 0.1); + color: var(--maintenance); +} + +.maintenance-description { + color: var(--text-secondary); + margin-bottom: 1rem; + line-height: 1.6; +} + +.maintenance-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Historical Uptime */ +.historical-uptime { + padding: 4rem 0; + background: var(--surface); +} + +.uptime-calendar { + display: grid; + grid-template-columns: repeat(13, 1fr); + gap: 0.25rem; + margin-bottom: 2rem; + max-width: 800px; + margin-left: auto; + margin-right: auto; +} + +.uptime-week { + display: grid; + grid-template-rows: repeat(7, 1fr); + gap: 0.25rem; +} + +.uptime-day { + width: 12px; + height: 12px; + border-radius: 2px; + cursor: pointer; + transition: transform 0.2s ease; +} + +.uptime-day:hover { + transform: scale(1.2); +} + +.uptime-100 { + background: #10b981; +} + +.uptime-99 { + background: #34d399; +} + +.uptime-95 { + background: #fbbf24; +} + +.uptime-90 { + background: #f87171; +} + +.uptime-down { + background: #ef4444; +} + +.uptime-legend { + display: flex; + justify-content: center; + gap: 2rem; + flex-wrap: wrap; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; +} + +/* Subscribe Section */ +.subscribe-section { + padding: 4rem 0; + background: var(--background); +} + +.subscribe-card { + background: white; + padding: 3rem; + border-radius: 1rem; + box-shadow: var(--shadow); + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; +} + +.subscribe-content h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.subscribe-content p { + color: var(--text-secondary); +} + +.subscribe-form { + display: flex; + gap: 1rem; + min-width: 300px; +} + +.subscribe-form input { + flex: 1; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + font-size: 1rem; +} + +.subscribe-form input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +/* Footer */ +.footer { + background: var(--text-primary); + color: white; + padding: 4rem 0 2rem; +} + +.footer-content { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 3rem; + margin-bottom: 3rem; +} + +.footer-logo { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1rem; +} + +.footer-section h4 { + font-weight: 600; + margin-bottom: 1rem; +} + +.footer-section ul { + list-style: none; +} + +.footer-section ul li { + margin-bottom: 0.5rem; +} + +.footer-section ul li a { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-section ul li a:hover { + color: white; +} + +.footer-bottom { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.footer-links { + display: flex; + gap: 2rem; +} + +.footer-links a { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-links a:hover { + color: white; +} + +/* Toast */ +.toast { + position: fixed; + top: 2rem; + right: 2rem; + background: var(--success); + color: white; + padding: 1rem 1.5rem; + border-radius: 0.5rem; + box-shadow: var(--shadow-lg); + transform: translateX(100%); + transition: transform 0.3s ease; + z-index: 1000; +} + +.toast.show { + transform: translateX(0); +} + +.toast-content { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.toast-icon { + font-size: 1.25rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .nav-menu { + display: none; + } + + .hero-title { + font-size: 2rem; + } + + .status-indicator { + flex-direction: column; + gap: 1rem; + } + + .metrics-grid { + grid-template-columns: 1fr; + } + + .charts-grid { + grid-template-columns: 1fr; + } + + .service-item { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .service-status { + align-self: flex-end; + } + + .subscribe-card { + flex-direction: column; + text-align: center; + } + + .subscribe-form { + min-width: auto; + width: 100%; + } + + .footer-content { + grid-template-columns: 1fr; + text-align: center; + } + + .footer-bottom { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .uptime-legend { + flex-direction: column; + gap: 1rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 0 1rem; + } + + .hero { + padding: 6rem 0 3rem; + } + + .section-title { + font-size: 2rem; + } + + .uptime-calendar { + grid-template-columns: repeat(7, 1fr); + } + + .toast { + top: 1rem; + right: 1rem; + left: 1rem; + } +} diff --git a/static/status.html b/static/status.html new file mode 100644 index 0000000..d806b05 --- /dev/null +++ b/static/status.html @@ -0,0 +1,297 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>System Status - TaskFlow</title> + <link rel="stylesheet" href="status-styles.css"> + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> +</head> +<body> + <nav class="navbar"> + <div class="nav-container"> + <a href="/" class="nav-logo">TaskFlow</a> + <ul class="nav-menu"> + <li><a href="/#features" class="nav-link">Features</a></li> + <li><a href="/#pricing" class="nav-link">Pricing</a></li> + <li><a href="about.html" class="nav-link">About</a></li> + <li><a href="contact.html" class="nav-link">Contact</a></li> + </ul> + <div class="nav-actions"> + <a href="login.html" class="btn btn-ghost">Sign In</a> + <button class="btn btn-primary">Start Free Trial</button> + </div> + </div> + </nav> + + <main> + <section class="hero"> + <div class="container"> + <div class="hero-content"> + <div class="status-indicator" id="overallStatus"> + <div class="status-icon" id="overallStatusIcon">🟢</div> + <div class="status-text"> + <h1 class="hero-title">All Systems <span class="status-label" id="overallStatusText">Operational</span></h1> + <p class="hero-subtitle" id="overallStatusDescription"> + All TaskFlow services are running smoothly + </p> + </div> + </div> + <div class="last-updated"> + Last updated: <span id="lastUpdated">--</span> + <button class="refresh-btn" id="refreshBtn" title="Refresh status"> + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> + <polyline points="23 4 23 10 17 10"></polyline> + <polyline points="1 20 1 14 7 14"></polyline> + <path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path> + </svg> + </button> + </div> + </div> + </div> + </section> + + <section class="status-overview"> + <div class="container"> + <div class="metrics-grid"> + <div class="metric-card"> + <div class="metric-header"> + <h3>Uptime</h3> + <div class="metric-status operational">99.9%</div> + </div> + <div class="metric-value" id="uptimeMetric">99.95%</div> + <div class="metric-label">Last 30 days</div> + </div> + <div class="metric-card"> + <div class="metric-header"> + <h3>Response Time</h3> + <div class="metric-status operational" id="responseTimeStatus">Good</div> + </div> + <div class="metric-value" id="responseTimeMetric">245ms</div> + <div class="metric-label">Average response time</div> + </div> + <div class="metric-card"> + <div class="metric-header"> + <h3>Active Incidents</h3> + <div class="metric-status operational" id="incidentsStatus">None</div> + </div> + <div class="metric-value" id="incidentsMetric">0</div> + <div class="metric-label">Current incidents</div> + </div> + <div class="metric-card"> + <div class="metric-header"> + <h3>Scheduled Maintenance</h3> + <div class="metric-status operational" id="maintenanceStatus">None</div> + </div> + <div class="metric-value" id="maintenanceMetric">0</div> + <div class="metric-label">Upcoming maintenance</div> + </div> + </div> + </div> + </section> + + <section class="services-status"> + <div class="container"> + <div class="section-header"> + <h2 class="section-title">Service Status</h2> + <p class="section-subtitle">Real-time status of all TaskFlow services</p> + </div> + + <div class="services-list" id="servicesList"> + <!-- Services will be populated by JavaScript --> + </div> + </div> + </section> + + <section class="performance-charts"> + <div class="container"> + <div class="section-header"> + <h2 class="section-title">Performance Metrics</h2> + <p class="section-subtitle">System performance over the last 24 hours</p> + </div> + + <div class="charts-grid"> + <div class="chart-card"> + <div class="chart-header"> + <h3>Response Time</h3> + <div class="chart-controls"> + <select id="responseTimeRange" class="time-range-select"> + <option value="1h">Last Hour</option> + <option value="6h">Last 6 Hours</option> + <option value="24h" selected>Last 24 Hours</option> + <option value="7d">Last 7 Days</option> + </select> + </div> + </div> + <div class="chart-container"> + <canvas id="responseTimeChart" width="400" height="200"></canvas> + </div> + </div> + + <div class="chart-card"> + <div class="chart-header"> + <h3>Request Volume</h3> + <div class="chart-controls"> + <select id="volumeRange" class="time-range-select"> + <option value="1h">Last Hour</option> + <option value="6h">Last 6 Hours</option> + <option value="24h" selected>Last 24 Hours</option> + <option value="7d">Last 7 Days</option> + </select> + </div> + </div> + <div class="chart-container"> + <canvas id="volumeChart" width="400" height="200"></canvas> + </div> + </div> + </div> + </div> + </section> + + <section class="incidents-section"> + <div class="container"> + <div class="section-header"> + <h2 class="section-title">Recent Incidents</h2> + <p class="section-subtitle">Latest incidents and their resolution status</p> + </div> + + <div class="incidents-list" id="incidentsList"> + <!-- Incidents will be populated by JavaScript --> + </div> + + <div class="no-incidents" id="noIncidents"> + <div class="no-incidents-icon">✅</div> + <h3>No Recent Incidents</h3> + <p>All systems have been running smoothly. No incidents to report in the last 30 days.</p> + </div> + </div> + </section> + + <section class="maintenance-section"> + <div class="container"> + <div class="section-header"> + <h2 class="section-title">Scheduled Maintenance</h2> + <p class="section-subtitle">Upcoming maintenance windows and system updates</p> + </div> + + <div class="maintenance-list" id="maintenanceList"> + <!-- Maintenance items will be populated by JavaScript --> + </div> + + <div class="no-maintenance" id="noMaintenance"> + <div class="no-maintenance-icon">🔧</div> + <h3>No Scheduled Maintenance</h3> + <p>No maintenance windows are currently scheduled. We'll notify you in advance of any planned maintenance.</p> + </div> + </div> + </section> + + <section class="historical-uptime"> + <div class="container"> + <div class="section-header"> + <h2 class="section-title">Historical Uptime</h2> + <p class="section-subtitle">90-day uptime history</p> + </div> + + <div class="uptime-calendar" id="uptimeCalendar"> + <!-- Calendar will be populated by JavaScript --> + </div> + + <div class="uptime-legend"> + <div class="legend-item"> + <div class="legend-color uptime-100"></div> + <span>100% uptime</span> + </div> + <div class="legend-item"> + <div class="legend-color uptime-99"></div> + <span>99-99.9% uptime</span> + </div> + <div class="legend-item"> + <div class="legend-color uptime-95"></div> + <span>95-99% uptime</span> + </div> + <div class="legend-item"> + <div class="legend-color uptime-90"></div> + <span>90-95% uptime</span> + </div> + <div class="legend-item"> + <div class="legend-color uptime-down"></div> + <span>Below 90%</span> + </div> + </div> + </div> + </section> + + <section class="subscribe-section"> + <div class="container"> + <div class="subscribe-card"> + <div class="subscribe-content"> + <h3>Stay Updated</h3> + <p>Subscribe to status updates and get notified about incidents and maintenance.</p> + </div> + <div class="subscribe-form"> + <form id="subscribeForm"> + <input type="email" placeholder="Enter your email" id="subscribeEmail" required> + <button type="submit" class="btn btn-primary">Subscribe</button> + </form> + </div> + </div> + </div> + </section> + </main> + + <footer class="footer"> + <div class="container"> + <div class="footer-content"> + <div class="footer-section"> + <div class="footer-logo">TaskFlow</div> + <p>Streamline your project management with the tools teams love to use.</p> + </div> + <div class="footer-section"> + <h4>Product</h4> + <ul> + <li><a href="/#features">Features</a></li> + <li><a href="/#pricing">Pricing</a></li> + <li><a href="#integrations">Integrations</a></li> + <li><a href="#security">Security</a></li> + </ul> + </div> + <div class="footer-section"> + <h4>Company</h4> + <ul> + <li><a href="about.html">About</a></li> + <li><a href="#careers">Careers</a></li> + <li><a href="blog.html">Blog</a></li> + <li><a href="#press">Press</a></li> + </ul> + </div> + <div class="footer-section"> + <h4>Support</h4> + <ul> + <li><a href="#help">Help Center</a></li> + <li><a href="contact.html">Contact</a></li> + <li><a href="status.html">Status</a></li> + <li><a href="#api">API Docs</a></li> + </ul> + </div> + </div> + <div class="footer-bottom"> + <p>© 2025 TaskFlow. All rights reserved.</p> + <div class="footer-links"> + <a href="#privacy">Privacy Policy</a> + <a href="#terms">Terms of Service</a> + </div> + </div> + </div> + </footer> + + <div class="toast" id="toast"> + <div class="toast-content"> + <span class="toast-icon">✓</span> + <span class="toast-message"></span> + </div> + </div> + + <script src="status-script.js"></script> +</body> +</html> |