diff --git a/MAINTENANCE_WINDOWS.md b/MAINTENANCE_WINDOWS.md new file mode 100644 index 0000000..e2b31cf --- /dev/null +++ b/MAINTENANCE_WINDOWS.md @@ -0,0 +1,83 @@ +# Maintenance Windows Feature + +This document describes the maintenance windows feature added to the WATcloud status page. + +## Overview + +The maintenance windows feature displays scheduled maintenance, ongoing maintenance, and completed maintenance windows on the status page. This helps users understand when planned outages or service interruptions might occur. + +## Components + +### MaintenanceWindows Component (`src/maintenance.tsx`) + +The main component that displays maintenance window information with: + +- **Visual Status Indicators**: Different emoji icons for each status type +- **Status-based Styling**: Color-coded backgrounds and text +- **Responsive Design**: Works on desktop and mobile devices +- **Dark Mode Support**: Adapts to the theme selection +- **Time Calculations**: Shows relative time ("starts in", "ends in") +- **Sorting**: Prioritizes ongoing maintenance, then upcoming, then completed + +### Types and Constants (`src/constants.ts`) + +Added the following types and enums: + +- `MaintenanceStatus`: Enum for upcoming, ongoing, completed +- `MaintenanceWindow`: Interface defining the data structure +- `MAINTENANCE_SYMBOLS`: Emoji symbols for each status + +### Utility Functions (`src/utils.ts`) + +Added `timeUntil()` function to calculate time until a future date. + +## Data Structure + +Each maintenance window contains: + +```typescript +interface MaintenanceWindow { + id: string; // Unique identifier + title: string; // Display title + description: string; // Detailed description + startTime: Date; // When maintenance starts + endTime: Date; // When maintenance ends + status: MaintenanceStatus; // Current status (calculated) + affectedServices?: string[]; // Optional list of affected services + detailsUrl?: string; // Optional link to more details +} +``` + +## Features + +1. **Automatic Status Detection**: Status is calculated based on current time vs. start/end times +2. **Smart Sorting**: Ongoing maintenance appears first, then upcoming, then completed +3. **Responsive Design**: Adapts to different screen sizes +4. **Theme Support**: Works with light, dark, and auto themes +5. **Empty State**: Shows appropriate message when no maintenance is scheduled +6. **External Links**: Optional "More Details" links to announcements +7. **Time Display**: Shows both absolute times and relative times +8. **Service Information**: Lists affected services when available + +## Usage + +The component is automatically included in the main status page. To modify maintenance windows: + +1. **For Demo/Testing**: Update the `SAMPLE_MAINTENANCE_WINDOWS` array in `src/maintenance.tsx` +2. **For Production**: Replace the sample data with an API call or external data source + +## Integration Points + +- Integrated into `App.tsx` between the Options and Healthchecks.io sections +- Uses existing utility functions and styling patterns +- Follows the same design language as other status sections + +## Future Enhancements + +The current implementation uses static sample data. Future enhancements could include: + +- Integration with a calendar service (Google Calendar, Outlook, etc.) +- API endpoint for dynamic maintenance window management +- Admin interface for adding/editing maintenance windows +- Email notifications for upcoming maintenance +- Integration with existing monitoring tools \ No newline at end of file diff --git a/src/App.css b/src/App.css index 4e33d7b..68a52fe 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,12 @@ #root { margin: 0 auto; padding: 2rem; - text-align: center; + text-align: left; + min-height: 100vh; } .main-logo > svg { - height: 5rem; + height: 6rem; mask-image: linear-gradient( 60deg, black 25%, @@ -14,6 +15,7 @@ ); mask-size: 400%; mask-position: 0%; + transition: mask-position 1s ease; } .main-logo > svg:hover { mask-position: 100%; @@ -23,8 +25,10 @@ } .main-logo { - @apply text-inherit; - &:hover { - @apply text-inherit; - } + color: inherit; + display: inline-block; +} + +.main-logo:hover { + color: inherit; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ad415cb..37a19c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { HealthchecksioStatus } from './healthchecksio' import { useState } from 'react' import { SentryStatus } from './sentry' import { OptionGroup } from './option-group' +import { MaintenanceWindows } from './maintenance' function updateQueryParams(key: string, val: string, queryParams: URLSearchParams) { queryParams.set(key, val); @@ -74,8 +75,8 @@ function App() {
-

Quick Links

-
-

Options

+

Options

- Theme: + Theme:
- + setShowInternal(!showInternal)} />
-
-

Healthchecks.io

-

Monitoring data from healthchecks.io

- -
-
-

Sentry

-

Monitoring data from watonomous.sentry.io

- + +
+
+

Scheduled Maintenance

+

Planned maintenance windows and outages

+ +
+ +
+

Healthchecks.io

+

Monitoring data from healthchecks.io

+ +
+ +
+

Sentry

+

Monitoring data from watonomous.sentry.io

+ +
) diff --git a/src/constants.ts b/src/constants.ts index 1f03917..df49027 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -29,4 +29,27 @@ export const STATUS_SYMBOLS = { [Status.Good]: '🟢', [Status.Neutral]: '🟡', [Status.Bad]: '🔴', -} as const; \ No newline at end of file +} as const; + +export enum MaintenanceStatus { + Upcoming = 'upcoming', + Ongoing = 'ongoing', + Completed = 'completed', +} + +export const MAINTENANCE_SYMBOLS = { + [MaintenanceStatus.Upcoming]: '🔔', + [MaintenanceStatus.Ongoing]: '🔧', + [MaintenanceStatus.Completed]: '✅', +} as const; + +export interface MaintenanceWindow { + id: string; + title: string; + description: string; + startTime: Date; + endTime: Date; + status: MaintenanceStatus; + affectedServices?: string[]; + detailsUrl?: string; +} \ No newline at end of file diff --git a/src/maintenance.tsx b/src/maintenance.tsx new file mode 100644 index 0000000..ce8868c --- /dev/null +++ b/src/maintenance.tsx @@ -0,0 +1,201 @@ +import { MaintenanceWindow, MaintenanceStatus, MAINTENANCE_SYMBOLS } from "./constants"; +import { timeUntil, cn } from "./utils"; + +// Sample maintenance windows data - in a real implementation, this could come from an API +const SAMPLE_MAINTENANCE_WINDOWS: MaintenanceWindow[] = [ + { + id: "maint-001", + title: "Scheduled Network Maintenance", + description: "Upgrading network infrastructure in the compute cluster. Some services may experience brief interruptions.", + startTime: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + endTime: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000), // 4 hours later + status: MaintenanceStatus.Upcoming, + affectedServices: ["Compute Cluster", "File Storage"], + detailsUrl: "https://cloud.watonomous.ca/docs/compute-cluster/announcements" + }, + { + id: "maint-002", + title: "Database Optimization", + description: "Performing routine database maintenance and optimization tasks.", + startTime: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago + endTime: new Date(Date.now() + 1 * 60 * 60 * 1000), // 1 hour from now + status: MaintenanceStatus.Ongoing, + affectedServices: ["Web Dashboard", "API"], + } +]; + +function getMaintenanceStatus(window: MaintenanceWindow): MaintenanceStatus { + const now = new Date(); + if (now < window.startTime) { + return MaintenanceStatus.Upcoming; + } else if (now > window.endTime) { + return MaintenanceStatus.Completed; + } else { + return MaintenanceStatus.Ongoing; + } +} + +function getStatusClassName(status: MaintenanceStatus): string { + switch (status) { + case MaintenanceStatus.Upcoming: + return "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950"; + case MaintenanceStatus.Ongoing: + return "border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950"; + case MaintenanceStatus.Completed: + return "border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950"; + default: + return "border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900"; + } +} + +function getStatusTextClassName(status: MaintenanceStatus): string { + switch (status) { + case MaintenanceStatus.Upcoming: + return "text-blue-800 dark:text-blue-200"; + case MaintenanceStatus.Ongoing: + return "text-amber-800 dark:text-amber-200"; + case MaintenanceStatus.Completed: + return "text-green-800 dark:text-green-200"; + default: + return "text-gray-800 dark:text-gray-200"; + } +} + +function formatDateTime(date: Date): string { + return date.toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short' + }); +} + +function formatDuration(startTime: Date, endTime: Date): string { + const durationMs = endTime.getTime() - startTime.getTime(); + const hours = Math.floor(durationMs / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 0) { + return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; + } + return `${minutes}m`; +} + +interface MaintenanceWindowsProps { + showCompleted?: boolean; +} + +export function MaintenanceWindows({ showCompleted = false }: MaintenanceWindowsProps) { + // Filter maintenance windows based on preferences and update their status + const maintenanceWindows = SAMPLE_MAINTENANCE_WINDOWS + .map(window => ({ + ...window, + status: getMaintenanceStatus(window) + })) + .filter(window => showCompleted || window.status !== MaintenanceStatus.Completed) + .sort((a, b) => { + // Sort by status priority (ongoing first, then upcoming, then completed) + const statusPriority = { + [MaintenanceStatus.Ongoing]: 0, + [MaintenanceStatus.Upcoming]: 1, + [MaintenanceStatus.Completed]: 2 + }; + + if (statusPriority[a.status] !== statusPriority[b.status]) { + return statusPriority[a.status] - statusPriority[b.status]; + } + + // Within same status, sort by start time + return a.startTime.getTime() - b.startTime.getTime(); + }); + + if (maintenanceWindows.length === 0) { + return ( +
+
+

+ No scheduled maintenance windows at this time. All systems are operating normally. +

+
+
+ ); + } + + return ( +
+
+ {maintenanceWindows.map((window) => ( +
+
+
+ + {MAINTENANCE_SYMBOLS[window.status]} + +

{window.title}

+ + {window.status} + +
+ {window.detailsUrl && ( + + Details + + )} +
+ +

{window.description}

+ +
+ Start: {formatDateTime(window.startTime)} + End: {formatDateTime(window.endTime)} + Duration: {formatDuration(window.startTime, window.endTime)} +
+ + {(window.status === MaintenanceStatus.Upcoming || window.status === MaintenanceStatus.Ongoing) && ( +
+ + {window.status === MaintenanceStatus.Upcoming ? "Starts in: " : "Ends in: "} + + + {window.status === MaintenanceStatus.Upcoming + ? timeUntil(window.startTime) + : timeUntil(window.endTime)} + +
+ )} + + {window.affectedServices && window.affectedServices.length > 0 && ( +
+ Affected Services: + {window.affectedServices.map((service, index) => ( + + {service} + + ))} +
+ )} +
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 9570218..9950982 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -98,6 +98,32 @@ export function timeSince(date: Date): string { return v + ' day' + (v === 1 ? '' : 's'); } +export function timeUntil(date: Date): string { + let v: number = Math.floor((date.getTime() - new Date().getTime()) / 1000); + + if (v <= 0) { + return 'now'; + } + + if (v < 60) { + // v is seconds + return v + ' second' + (v === 1 ? '' : 's'); + } + + v = Math.floor(v / 60); // v is now minutes + if (v < 60) { + return v + ' minute' + (v === 1 ? '' : 's'); + } + + v = Math.floor(v / 60); // v is now hours + if (v < 24) { + return v + ' hour' + (v === 1 ? '' : 's'); + } + + v = Math.floor(v / 24); // v is now days + return v + ' day' + (v === 1 ? '' : 's'); +} + export function timeSinceShort(date: Date): string { const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); const units: [number, string][] = [