summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobby Zambito <contact@robbyzambito.me>2025-08-06 19:51:58 -0400
committerRobby Zambito <contact@robbyzambito.me>2025-08-06 19:58:08 -0400
commite33771f6adae7d9664f983b96abad28a27b61a8b (patch)
tree35597a3253feab0f0d77ead2aae86f3aae808ecd
parenta5ac279f6882d27b92e482125a87635d9c1ad00d (diff)
Created status page
Prompt: Create a status page with live updating data that polls API endpoints for data
-rw-r--r--static/about.html2
-rw-r--r--static/blog.html2
-rw-r--r--static/contact.html2
-rw-r--r--static/index.html2
-rw-r--r--static/status-script.js667
-rw-r--r--static/status-styles.css921
-rw-r--r--static/status.html297
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>&copy; 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>