10 Commits

9 changed files with 344 additions and 542 deletions

View File

@@ -1,5 +1,45 @@
# Changelog
## 2025-12-28 - 1.4.2 - fix(deps)
add MIT license, update package metadata and bump dependencies; simplify docs
- Added license.md containing the MIT license and set package.json license to MIT
- Updated package.json author to Task Venture Capital GmbH
- Bumped @design.estate/dees-wcctools from ^3.2.0 to ^3.3.0 and @uptime.link/interfaces from ^2.0.21 to ^2.1.0
- Updated readme.md to reference license.md instead of LICENSE
- Rewrote and condensed readme.hints.md (cleaned up project overview, components list, build output and commands; removed legacy/verbose content)
## 2025-12-24 - 1.4.1 - fix(statuspage)
no changes detected; no version bump
- No files changed in the provided git diff
- package.json version is 1.4.0 — no version bump recommended
## 2025-12-24 - 1.4.0 - feat(statuspage-incidents)
Add status icons/labels and revamp incident header layout; replace left border with internal severity bar and clean up formatting
- Introduce TIncidentStatus and mappings for statusIcons and statusLabels for consistent iconography and human-readable labels
- Update incident header layout to match admin catalog (adjust padding, alignment, spacing)
- Replace border-left severity indicator with an internal .incident-severity element and apply severity-specific styles
- Minor code/style cleanups: whitespace fixes in formatDate/formatDuration and normalized event detail object formatting
## 2025-12-23 - 1.3.1 - fix(statuspage)
update timeline connector and dot positioning in statuspage incidents component
- Remove global .timeline::before vertical connector and replace with per-item .update-item:not(:last-child)::after to draw connectors between dots
- Use sharedStyles.colors.border.default for the connector background instead of a theme gradient
- Adjust connector and dot offsets for desktop and mobile to align with dot size and border, improving visual alignment
- Add clarifying comments about dot dimensions and center calculations
## 2025-12-23 - 1.3.0 - feat(statuspage)
use dynamic status-based accent colors and computed card statuses; update stat card markup and incident/response displays
- Replace hardcoded stat-card type selectors with status-based classes (.operational, .degraded, .partial_outage, .major_outage, .maintenance) so accent colors are applied centrally.
- Introduce getUptimeCardStatus, getResponseCardStatus, and getIncidentCardStatus helper methods to compute and apply per-card status classes based on uptime, response time, and affected services.
- Update stat card markup to use computed status classes instead of dedicated uptime/response/incident class names.
- Change incident summary text to show 'All services ok.' when no services are affected, otherwise display 'X of Y services affected'.
- Minor layout tweak: adjust timeline connector left offset from 5px to 7px for improved alignment.
## 2025-12-23 - 1.2.0 - feat(statuspage-ui)
improve styling and animations across status page components

21
license.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,6 @@
{
"name": "@uptime.link/statuspage",
"version": "1.2.0",
"version": "1.4.2",
"private": false,
"description": "A catalog of web components for the UptimeLink dashboard.",
"main": "dist_ts_web/index.js",
@@ -12,13 +12,13 @@
"watch": "tswatch element",
"buildDocs": "tsdoc"
},
"author": "Lossless GmbH",
"license": "UNLICENSED",
"author": "Task Venture Capital GmbH",
"license": "MIT",
"dependencies": {
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^3.2.0",
"@uptime.link/interfaces": "^2.0.21"
"@design.estate/dees-wcctools": "^3.3.0",
"@uptime.link/interfaces": "^2.1.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.0.2",

44
pnpm-lock.yaml generated
View File

@@ -15,11 +15,11 @@ importers:
specifier: ^2.1.3
version: 2.1.3
'@design.estate/dees-wcctools':
specifier: ^3.2.0
version: 3.2.0
specifier: ^3.3.0
version: 3.3.0
'@uptime.link/interfaces':
specifier: ^2.0.21
version: 2.0.21
specifier: ^2.1.0
version: 2.1.0
devDependencies:
'@git.zone/tsbuild':
specifier: ^4.0.2
@@ -62,9 +62,6 @@ packages:
peerDependencies:
'@push.rocks/smartserve': '>=1.1.0'
'@apiglobal/typedrequest-interfaces@2.0.1':
resolution: {integrity: sha512-Oi7pNU4vKo5UvcCJmqkH43Us237Ws/Pp/WDYnwnonRnTmIMd+6QjNfN/gXcPnP6tbamk8r8Xzcz9mgnSDM2ysw==}
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
engines: {node: '>=16.0.0'}
@@ -256,8 +253,8 @@ packages:
'@design.estate/dees-element@2.1.3':
resolution: {integrity: sha512-TjXWxVcdSPaT1IOk31ckfxvAZnJLuTxhFGsNCKoh63/UE2FVf6slp8//UFvN+ADigiA9ZsY0azkY99XbJCwDDA==}
'@design.estate/dees-wcctools@3.2.0':
resolution: {integrity: sha512-tBh4RJFsQFIXDLzrksFK/M1i/34bMcDLFhYO/MWW5ArgH3mDyRlg+10RMAmbjI9ijOnegbbEtWanHoDx9EKgUA==}
'@design.estate/dees-wcctools@3.3.0':
resolution: {integrity: sha512-ZOxG5LkbLLsqDQWO+JCOjFkL77l9FuLDa7LBuZRkTSX0jRoYG6ICI1UoI9i6twxm4JKSzQ4iHjL/F5mHbQiKTg==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@@ -1404,9 +1401,6 @@ packages:
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@tsclass/tsclass@4.0.61':
resolution: {integrity: sha512-E+LEX/cpRy0uMOJ3oQz3rBqfeMyCraVPeromT7VH9esYxUPh2SiRMIN6Im250IHXx7ANl0TNyX27UmbaD347GQ==}
'@tsclass/tsclass@4.4.4':
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
@@ -1515,8 +1509,8 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@uptime.link/interfaces@2.0.21':
resolution: {integrity: sha512-sy7WBzHOxU7Kt0BGofK0R3CS8D8QtbTXB00i75QKYXkEesdLd91SbFL80wTupKfjzeldE0ejUVSgvtlZEr8XlQ==}
'@uptime.link/interfaces@2.1.0':
resolution: {integrity: sha512-KlsAUp4Vvb4TeFNBDq4MZ9v4RaeLPr1GRlj4mkNmGLSfEiSasj1FWGLEj8iRdfeUuxMu0WRpAvWe2Ol0MRzoqg==}
'@webcontainer/api@1.2.0':
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
@@ -2791,10 +2785,6 @@ packages:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
type-fest@4.20.1:
resolution: {integrity: sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==}
engines: {node: '>=16'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
@@ -3005,8 +2995,6 @@ snapshots:
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
'@apiglobal/typedrequest-interfaces@2.0.1': {}
'@aws-crypto/crc32@5.2.0':
dependencies:
'@aws-crypto/util': 5.2.0
@@ -3508,7 +3496,7 @@ snapshots:
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
'@design.estate/dees-wcctools': 3.2.0
'@design.estate/dees-wcctools': 3.3.0
'@fortawesome/fontawesome-svg-core': 7.1.0
'@fortawesome/free-brands-svg-icons': 7.1.0
'@fortawesome/free-regular-svg-icons': 7.1.0
@@ -3585,7 +3573,7 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-wcctools@3.2.0':
'@design.estate/dees-wcctools@3.3.0':
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
@@ -5172,10 +5160,6 @@ snapshots:
'@tokenizer/token@0.3.0': {}
'@tsclass/tsclass@4.0.61':
dependencies:
type-fest: 4.20.1
'@tsclass/tsclass@4.4.4':
dependencies:
type-fest: 4.41.0
@@ -5289,10 +5273,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@uptime.link/interfaces@2.0.21':
'@uptime.link/interfaces@2.1.0':
dependencies:
'@apiglobal/typedrequest-interfaces': 2.0.1
'@tsclass/tsclass': 4.0.61
'@api.global/typedrequest-interfaces': 3.0.19
'@tsclass/tsclass': 9.3.0
'@webcontainer/api@1.2.0': {}
@@ -6855,8 +6839,6 @@ snapshots:
type-fest@2.19.0: {}
type-fest@4.20.1: {}
type-fest@4.41.0: {}
typescript@5.9.3: {}

View File

@@ -1,292 +1,48 @@
# Project Hints and Analysis
# Project Hints
## Project Overview
The @uptime.link/statuspage (v1.0.74) is a web components catalog specifically designed for building status pages for UptimeLink - an uptime monitoring platform. This catalog provides pre-built, customizable UI components that can be assembled to create complete status pages.
## Core Purpose
- **Primary Function**: Provide a comprehensive set of web components for building status monitoring dashboards
- **Target Audience**: Developers building status pages for services using UptimeLink monitoring
- **Key Features**: Real-time status display, incident management, historical data visualization
**@uptime.link/statuspage** - A comprehensive web components catalog for building status pages for UptimeLink.
## Architecture
- **Package Name**: @uptime.link/statuspage (v1.0.74)
- **Project Type**: Web Component Catalog (wcc)
- **Module Type**: ESM (ECMAScript Modules)
- **Distribution**: Private npm package on verdaccio.lossless.one registry
- **License**: UNLICENSED (proprietary to Lossless GmbH)
- **Package Type**: Web Component Catalog (wcc)
- **Module Type**: ESM
- **License**: MIT
- **Registry**: Public on npm and verdaccio.lossless.digital
## Technology Stack
- **Framework**: @design.estate/dees-element - A web components framework with:
- TypeScript decorators for component registration
- Built-in CSS-in-JS with theme support
- Shadow DOM encapsulation
- Property binding system
- **DOM Utilities**: @design.estate/dees-domtools for DOM manipulation
- **Interfaces**: @uptime.link/interfaces for shared data structures
- **Build Tools**:
- tsbuild: TypeScript compilation with --allowimplicitany flag
- tsbundle: Creates production bundles for web components
- tswatch: Development file watching
- TypeScript target: ES2022 with NodeNext module resolution
- **Framework**: @design.estate/dees-element (Lit-based web components)
- **Build Tools**: tsbuild, tsbundle, tswatch
- **TypeScript**: ES2022 target with NodeNext module resolution
## Component Library
The catalog provides 7 main components + 1 internal component:
9 components total:
1. `upl-statuspage-header` - Page header with branding and action buttons
2. `upl-statuspage-pagetitle` - Hero section with title/subtitle
3. `upl-statuspage-statusbar` - Overall system status indicator
4. `upl-statuspage-statsgrid` - Key metrics grid (uptime, response time, incidents)
5. `upl-statuspage-assetsselector` - Service selection with filtering
6. `upl-statuspage-statusdetails` - 48-hour status timeline
7. `upl-statuspage-statusmonth` - Monthly calendar uptime view
8. `upl-statuspage-incidents` - Current and past incidents feed
9. `upl-statuspage-footer` - Full-featured footer with social/subscription
### Main Components:
1. **upl-statuspage-header**:
- Page header with customizable title
- Action buttons for "Report Incident" and "Subscribe to Updates"
- Emits custom events: 'reportNewIncident' and 'statusSubscribe'
## Build Output
- Source: `ts_web/`
- Compiled: `dist_ts_web/`
- Bundle: `dist_bundle/`
- Development: `dist_watch/`
2. **upl-statuspage-statusbar**:
- Main status indicator showing overall system health
- Visual status representation (likely green/yellow/red indicators)
## Commands
- `pnpm build` - Compile TypeScript and create bundle
- `pnpm watch` - Development server with hot reload
- `pnpm test` - Runs build (no actual tests)
3. **upl-statuspage-assetsselector**:
- Component for selecting/filtering which assets to view
- Useful for multi-service status pages
## Demo System
- Uses `dees-demowrapper` from `@design.estate/dees-wcctools/demotools`
- Each component has a `.demo.ts` file with runAfterRender callbacks
- Pre-built page templates in `ts_web/pages/`
4. **upl-statuspage-statusdetails**:
- Detailed status information display
- Shows granular status data for selected services
5. **upl-statuspage-statusmonth**:
- Monthly calendar view of status history
- Visual representation of uptime/downtime over time
6. **upl-statuspage-incidents**:
- Incident management display
- Properties: currentIncidences and pastIncidences (arrays of IIncident)
- Supports whitelabel mode
- Shows active and historical incidents
7. **upl-statuspage-footer**:
- Page footer with legal information link
- Customizable legal URL
### Internal Components:
- **uplinternal-miniheading**: Internal component for consistent heading styling
## Data Flow & Integration
- Components receive data through properties (using @property decorator)
- Incident data follows the IIncident interface from @uptime.link/interfaces
- Components are designed to work standalone or together
- Event-driven communication between components
## Styling & Theming
- CSS-in-JS approach using cssManager
- Built-in light/dark theme support via bdTheme() helper
- Font: Inter (loaded via assetbroker)
- Responsive design with max-width constraints (900px)
- Background colors: Light (#eeeeeb) / Dark (#222222)
- Text colors: Light (#333333) / Dark (#ffffff)
## Build Output Structure
- Source: ts_web/ directory
- Compiled output: dist_ts_web/ (ES modules with TypeScript definitions)
- Bundle output: dist_bundle/ (production-ready bundle with source maps)
- Development server: dist_watch/ with index.html for testing
## Usage Pattern
1. Import components from the package
2. Create elements using document.createElement()
3. Set properties programmatically
4. Append to DOM
5. Handle custom events for user interactions
## Recent Updates (from changelog)
- v1.0.74: Improved font loading strategy using single assetbroker link
- v1.0.73: Enhanced documentation and aligned project descriptions
- v1.0.72: Fixed import paths and updated package configurations
## Development Workflow
- `pnpm build`: Compile TypeScript and create production bundle
- `pnpm watch`: Start development server with hot reload
- `pnpm test`: Currently just runs build (no actual tests implemented)
- Demo page available at html/index.html using page1 template
## Key Observations
1. The project follows a consistent pattern for all components
2. Each component is self-contained with its own styling
3. Theme support is built-in for all components
4. The project is part of a larger UptimeLink ecosystem
5. Components are designed for composition into complete status pages
6. No test files are currently implemented despite test infrastructure being set up
## Production Readiness Analysis (v1.0.74)
### Current State
The components are essentially **UI shells** - they have styling and structure but lack actual functionality. They display static/hardcoded content with no real data integration.
### Major Missing Functionality
#### 1. Data Integration
- **No API client or data fetching logic** - components can't retrieve real status data
- **No authentication/authorization** - no secure API communication
- **No real-time updates** - no WebSocket/SSE implementation
- **Static content only** - statusbar always shows "Everything is working normally!"
- **Empty data properties** - currentIncidences/pastIncidences arrays are never populated
#### 2. Component Implementation Gaps
- **upl-statuspage-assetsselector**: Only shows "Hello!" - missing entire asset selection UI
- **upl-statuspage-statusbar**: Hardcoded green status - no dynamic status calculation
- **upl-statuspage-statusdetails**: Shows 48 static green bars - no actual hourly data
- **upl-statuspage-statusmonth**: Shows 150 static green days - no real uptime data
- **upl-statuspage-incidents**: Only shows "No incidents" - missing incident card rendering
- **upl-statuspage-footer**: Placeholder "Hi there" - missing actual footer content
#### 3. Error Handling & States
- No loading indicators during data fetch
- No error states for failed requests
- No offline detection or handling
- No retry mechanisms
- No skeleton screens
#### 4. Accessibility Issues
- No ARIA labels on interactive elements
- No keyboard navigation support
- No focus management
- No screen reader announcements
- Missing semantic HTML (divs instead of buttons/nav)
- No skip navigation links
#### 5. Responsive Design Issues
- Fixed 900px max-width with no proper mobile breakpoints
- Grid layouts won't adapt to small screens
- No touch-friendly tap targets
- Font sizes not responsive
#### 6. Internationalization
- All text hardcoded in English
- No i18n framework or translation system
- No locale-aware date/time formatting
- No RTL language support
#### 7. Missing Infrastructure
- No configuration system for API endpoints
- No analytics integration
- No performance monitoring
- No PWA capabilities
- No export functionality
- No proper TypeScript interfaces for data models
- **No tests whatsoever** despite test infrastructure
### Production Requirements Summary
To make these components production-ready requires implementing:
1. Complete data layer with API client
2. State management system
3. All missing UI functionality
4. Comprehensive error handling
5. Full accessibility compliance
6. Proper responsive design
7. Internationalization support
8. Authentication/authorization
9. Real-time update capabilities
10. Comprehensive test suite
## Recent Updates (Post v1.0.74)
### Component Order (Top to Bottom)
1. `upl-statuspage-header` - Navigation header
2. `upl-statuspage-statusbar` - Overall system status
3. `upl-statuspage-statsgrid` - Key metrics grid (uptime, response time, incidents)
4. `upl-statuspage-assetsselector` - Service selection
5. `upl-statuspage-statusdetails` - 48-hour status visualization
6. `upl-statuspage-statusmonth` - Monthly calendar view
7. `upl-statuspage-incidents` - Current and past incidents
8. `upl-statuspage-footer` - Page footer
### Components Made Production-Ready
All components have been significantly enhanced with the following improvements:
1. **upl-statuspage-header**
- Added properties: showReportButton, showSubscribeButton, brandColor, logoUrl, customStyles, loading
- Supports custom branding with dynamic colors
- Loading state with skeleton animation
- Configurable button visibility
2. **upl-statuspage-statusbar**
- Already production-ready with full functionality
- Supports all status states (operational, degraded, partial_outage, major_outage, maintenance)
- Loading state and expandable behavior
3. **upl-statuspage-assetsselector**
- Complete implementation with service selection grid
- Full filtering capabilities (text, category, selected-only)
- Select all/none functionality
- Real-time status updates
- Event emissions for selection changes
- Loading states and empty states
4. **upl-statuspage-statusdetails**
- Hourly status bars with tooltips
- Skeleton loading states
- Real-time data updates
- Important: Expects hourly-aligned timestamps in data
5. **upl-statuspage-statusmonth**
- Calendar grid display with status colors
- Weekday labels and proper month alignment
- Hover tooltips with detailed information
- Day click events
6. **upl-statuspage-incidents**
- Full incident management with current/past incidents
- Multiple incident statuses (investigating, identified, monitoring, resolved, postmortem)
- Incident updates timeline
- Affected services display
- Root cause and resolution information
7. **upl-statuspage-footer** (Completely rebuilt)
- Comprehensive footer implementation with all expected properties
- Social media links with SVG icons (Twitter, GitHub, LinkedIn, Facebook, YouTube, Instagram, Slack, Discord)
- Subscribe/Report issue functionality
- Language selector and theme toggle
- Whitelabel support
- Custom branding options
- Loading and error states
- RSS feed and API status links
- Last updated timestamp with relative formatting
8. **upl-statuspage-statsgrid** (NEW)
- Displays key performance metrics in a responsive grid
- Shows current status with color indicator
- Uptime percentage with configurable time period
- Average response time with performance trend indicators
- Total incidents count with affected services
- Loading state with skeleton animation
- Responsive design that stacks on mobile
- Used stats data that was previously in statusdetails component
### Demo Architecture
- All demos have been updated to use dees-demowrapper with runAfterRender callbacks
- Properties are set dynamically on elements within runAfterRender
- Multiple demo sections show different use cases and states
- Event logging demonstrates interactivity
- Demos can be instrumented with multiple wrappers for different scenarios
### Interfaces Implemented
Created comprehensive TypeScript interfaces in ts_web/interfaces/index.ts:
- IServiceStatus - Service monitoring data
- IOverallStatus - Overall system status
- IIncidentUpdate - Incident update entries
- IIncidentDetails - Full incident information
- IMonthlyUptime - Monthly uptime calendar data
- IStatusHistoryPoint - Hourly status data points
- IStatusPageConfig - Configuration options
### Remaining Tasks
- Integration with actual UptimeLink API
- WebSocket/SSE for real-time updates
- Authentication/authorization implementation
- Accessibility improvements (ARIA labels, keyboard navigation)
- More comprehensive responsive design
- Internationalization system
- Unit and integration tests
### Important Fix Applied
The `dees-demowrapper` component was not functioning because it wasn't being imported. Fixed by adding:
```typescript
import '@design.estate/dees-wcctools/demotools';
```
to `html/index.ts`. This registers the `dees-demowrapper` custom element which properly executes the `runAfterRender` callbacks in demos.
## Important Notes
- Import `@design.estate/dees-wcctools/demotools` in html/index.ts for demos to work
- All components support light/dark theme via cssManager.bdTheme()
- Components are designed to work standalone or composed together

View File

@@ -362,7 +362,7 @@ Components automatically adapt to light/dark mode using `cssManager.bdTheme()`.
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@uptime.link/statuspage',
version: '1.2.0',
version: '1.4.2',
description: 'A catalog of web components for the UptimeLink dashboard.'
}

View File

@@ -20,6 +20,8 @@ declare global {
}
}
type TIncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
@customElement('upl-statuspage-incidents')
export class UplStatuspageIncidents extends DeesElement {
// STATIC
@@ -68,6 +70,22 @@ export class UplStatuspageIncidents extends DeesElement {
})
private accessor subscribedIncidents: Set<string> = new Set();
private statusIcons: Record<TIncidentStatus, string> = {
investigating: 'lucide:Search',
identified: 'lucide:Target',
monitoring: 'lucide:Eye',
resolved: 'lucide:CheckCircle',
postmortem: 'lucide:FileText',
};
private statusLabels: Record<TIncidentStatus, string> = {
investigating: 'Investigating',
identified: 'Identified',
monitoring: 'Monitoring',
resolved: 'Resolved',
postmortem: 'Postmortem',
};
constructor() {
super();
}
@@ -171,112 +189,151 @@ export class UplStatuspageIncidents extends DeesElement {
}
}
/* New header layout matching admin catalog */
.incident-header {
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.xl)};
border-left: 4px solid;
display: flex;
align-items: start;
justify-content: space-between;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
align-items: flex-start;
gap: 16px;
padding: 16px;
cursor: pointer;
transition: background-color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
position: relative;
}
.incident-header:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.02)')};
}
.incident-header.critical {
border-left-color: ${sharedStyles.colors.status.major};
/* Internal severity bar (replacing border-left) */
.incident-severity {
width: 4px;
align-self: stretch;
border-radius: 2px;
flex-shrink: 0;
}
.incident-header.major {
border-left-color: ${sharedStyles.colors.status.partial};
.incident-severity.critical { background: ${sharedStyles.colors.status.major}; }
.incident-severity.major { background: ${sharedStyles.colors.status.partial}; }
.incident-severity.minor { background: ${sharedStyles.colors.status.degraded}; }
.incident-severity.maintenance { background: ${sharedStyles.colors.status.maintenance}; }
.incident-main {
flex: 1;
min-width: 0;
}
.incident-header.minor {
border-left-color: ${sharedStyles.colors.status.degraded};
}
.incident-header.maintenance {
border-left-color: ${sharedStyles.colors.status.maintenance};
}
.incident-title {
font-size: 17px;
font-weight: 600;
margin: 0;
line-height: 1.4;
letter-spacing: -0.01em;
}
.incident-meta {
.incident-title-row {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
font-size: 13px;
color: ${sharedStyles.colors.text.secondary};
align-items: center;
gap: 8px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.incident-meta span {
display: flex;
align-items: center;
gap: 4px;
.incident-title {
font-size: 15px;
font-weight: 600;
margin: 0;
color: ${sharedStyles.colors.text.primary};
}
/* Status badge inline with title */
.incident-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
font-weight: 500;
border-radius: 9999px;
text-transform: uppercase;
letter-spacing: 0.04em;
flex-shrink: 0;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.incident-status dees-icon {
--icon-size: 12px;
}
.incident-status.investigating {
background: ${cssManager.bdTheme('#fef3c7', '#78350f')};
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
background: ${cssManager.bdTheme('rgba(249, 115, 22, 0.1)', 'rgba(249, 115, 22, 0.2)')};
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
--icon-color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
.incident-status.identified {
background: ${cssManager.bdTheme('#e9d5ff', '#581c87')};
color: ${cssManager.bdTheme('#6b21a8', '#d8b4fe')};
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.2)')};
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
--icon-color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.incident-status.monitoring {
background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')};
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.incident-status.resolved {
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
color: ${cssManager.bdTheme('#047857', '#6ee7b7')};
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#4ade80')};
--icon-color: ${cssManager.bdTheme('#16a34a', '#4ade80')};
}
.incident-status.postmortem {
background: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#4b5563', '#d1d5db')};
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.2)')};
color: ${cssManager.bdTheme('#9333ea', '#c084fc')};
--icon-color: ${cssManager.bdTheme('#9333ea', '#c084fc')};
}
/* Pulse for investigating status */
.incident-status.investigating .status-dot {
animation: status-pulse 1.5s ease-in-out infinite;
.incident-meta {
display: flex;
align-items: center;
gap: 16px;
font-size: 12px;
color: ${sharedStyles.colors.text.secondary};
flex-wrap: wrap;
}
@keyframes status-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.2); }
.incident-meta-item {
display: flex;
align-items: center;
gap: 6px;
}
.incident-meta-item dees-icon {
--icon-size: 12px;
--icon-color: ${sharedStyles.colors.text.muted};
}
.incident-expand {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
color: ${sharedStyles.colors.text.muted};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
flex-shrink: 0;
}
.incident-expand:hover {
background: ${sharedStyles.colors.background.muted};
color: ${sharedStyles.colors.text.primary};
}
.incident-expand dees-icon {
--icon-size: 16px;
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.incident-expand.expanded dees-icon {
transform: rotate(180deg);
}
.incident-body {
padding: 0 ${unsafeCSS(sharedStyles.spacing.xl)} ${unsafeCSS(sharedStyles.spacing.xl)} ${unsafeCSS(sharedStyles.spacing.xl)};
padding: 0 16px 16px 36px;
animation: slideDown 0.3s ${unsafeCSS(sharedStyles.easings.default)};
}
@@ -351,21 +408,6 @@ export class UplStatuspageIncidents extends DeesElement {
padding-left: 24px;
}
/* Vertical connector line */
.timeline::before {
content: '';
position: absolute;
left: 5px;
top: 8px;
bottom: 8px;
width: 2px;
background: ${cssManager.bdTheme(
'linear-gradient(to bottom, #e5e7eb 0%, #d1d5db 50%, #e5e7eb 100%)',
'linear-gradient(to bottom, #27272a 0%, #3f3f46 50%, #27272a 100%)'
)};
border-radius: 1px;
}
.update-item {
position: relative;
padding-left: ${unsafeCSS(sharedStyles.spacing.lg)};
@@ -377,7 +419,16 @@ export class UplStatuspageIncidents extends DeesElement {
padding-bottom: 0;
}
/* Timeline dot */
.update-item:not(:last-child)::after {
content: '';
position: absolute;
left: -15px;
top: 18px;
bottom: 0;
width: 2px;
background: ${sharedStyles.colors.border.default};
}
.update-item::before {
content: '';
position: absolute;
@@ -531,54 +582,39 @@ export class UplStatuspageIncidents extends DeesElement {
background: ${cssManager.bdTheme('#dcfce7', '#065f46')};
}
.collapsed-hint {
font-size: 12px;
color: ${sharedStyles.colors.text.secondary};
text-align: center;
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
opacity: 0.8;
}
/* Expand icon animation */
.expand-icon {
transition: transform ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
}
.expand-icon.rotated {
transform: rotate(180deg);
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.incident-header {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
padding: 12px;
}
.incident-body {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
padding: 0 12px 12px 24px;
}
.incident-meta {
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
align-items: flex-start;
gap: 8px;
}
.timeline {
padding-left: 20px;
}
.timeline::before {
left: 4px;
}
.update-item::before {
left: -18px;
width: 10px;
height: 10px;
}
.update-item:not(:last-child)::after {
left: -12px;
top: 16px;
}
}
`,
];
@@ -594,16 +630,16 @@ export class UplStatuspageIncidents extends DeesElement {
` :
html`<div class="noIncidentBox">No incidents ongoing.</div>`
}
<uplinternal-miniheading>Past Incidents</uplinternal-miniheading>
${this.loading ? html`
<div class="loading-skeleton"></div>
<div class="loading-skeleton"></div>
` : this.pastIncidents.length ?
` : this.pastIncidents.length ?
this.pastIncidents.slice(0, 5).map(incident => this.renderIncident(incident, false)) :
html`<div class="noIncidentBox">No past incidents in the last ${this.daysToShow} days.</div>`
}
${this.pastIncidents.length > 5 && !this.loading ? html`
<div class="show-more">
<button class="show-more-button" @click=${this.handleShowMore}>
@@ -622,74 +658,54 @@ export class UplStatuspageIncidents extends DeesElement {
this.formatDuration(Date.now() - incident.startTime);
const isActive = isCurrent && latestUpdate?.status !== 'resolved';
const isExpanded = this.expandedIncidents.has(incident.id);
return html`
<div class="incident-card ${this.expandedIncidents.has(incident.id) ? 'expanded' : ''} ${isActive ? 'active-incident' : ''}">
<div class="incident-header ${incident.severity}" @click=${() => this.toggleIncident(incident.id)}>
<div>
<h3 class="incident-title">${incident.title}</h3>
<div class="incident-card ${isExpanded ? 'expanded' : ''} ${isActive ? 'active-incident' : ''}">
<div class="incident-header" @click=${() => this.toggleIncident(incident.id)}>
<div class="incident-severity ${incident.severity}"></div>
<div class="incident-main">
<div class="incident-title-row">
<h3 class="incident-title">${incident.title}</h3>
<span class="incident-status ${latestUpdate.status}">
<dees-icon .icon=${this.statusIcons[latestUpdate.status as TIncidentStatus]} .iconSize=${12}></dees-icon>
${this.statusLabels[latestUpdate.status as TIncidentStatus] || latestUpdate.status}
</span>
</div>
<div class="incident-meta">
<span>Started: ${this.formatDate(incident.startTime)}</span>
<span>Duration: ${duration}</span>
${incident.endTime ? html`
<span>Ended: ${this.formatDate(incident.endTime)}</span>
` : ''}
</div>
${!this.expandedIncidents.has(incident.id) ? html`
<div style="
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
font-size: 13px;
color: ${sharedStyles.colors.text.secondary};
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
">
${incident.impact ? html`
<span style="
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 500px;
">${incident.impact}</span>
` : ''}
<span style="
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
">
${incident.updates.length} update${incident.updates.length !== 1 ? 's' : ''}
</span>
</div>
` : ''}
</div>
<div style="display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.md)};">
<div class="incident-status ${latestUpdate.status}">
${this.getStatusIcon(latestUpdate.status)}
${latestUpdate.status.replace(/_/g, ' ')}
</div>
<div class="expand-icon" style="
font-size: 10px;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
transition: transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
${this.expandedIncidents.has(incident.id) ? 'transform: rotate(180deg);' : ''}
">
<span class="incident-meta-item">
<dees-icon .icon=${'lucide:Calendar'} .iconSize=${12}></dees-icon>
${this.formatDate(incident.startTime)}
</span>
<span class="incident-meta-item">
<dees-icon .icon=${'lucide:Clock'} .iconSize=${12}></dees-icon>
${duration}
</span>
<span class="incident-meta-item">
<dees-icon .icon=${'lucide:Server'} .iconSize=${12}></dees-icon>
${incident.affectedServices.length} service${incident.affectedServices.length !== 1 ? 's' : ''}
</span>
<span class="incident-meta-item">
<dees-icon .icon=${'lucide:MessageSquare'} .iconSize=${12}></dees-icon>
${incident.updates.length} update${incident.updates.length !== 1 ? 's' : ''}
</span>
</div>
</div>
<button class="incident-expand ${isExpanded ? 'expanded' : ''}">
<dees-icon .icon=${'lucide:ChevronDown'} .iconSize=${16}></dees-icon>
</button>
</div>
${this.expandedIncidents.has(incident.id) ? html`
${isExpanded ? html`
<div class="incident-body">
<div class="incident-impact">
<strong>Impact:</strong> ${incident.impact}
</div>
${incident.impact ? html`
<div class="incident-impact">
<strong>Impact:</strong> ${incident.impact}
</div>
` : ''}
${incident.affectedServices.length > 0 ? html`
<div class="affected-services">
<div class="affected-services-title">Affected Services:</div>
@@ -698,7 +714,7 @@ export class UplStatuspageIncidents extends DeesElement {
`)}
</div>
` : ''}
${incident.updates.length > 0 ? html`
<div class="incident-updates">
<h4 class="updates-title">Updates</h4>
@@ -707,21 +723,21 @@ export class UplStatuspageIncidents extends DeesElement {
</div>
</div>
` : ''}
${incident.rootCause && isCurrent === false ? html`
<div class="incident-impact" style="margin-top: 12px;">
<strong>Root Cause:</strong> ${incident.rootCause}
</div>
` : ''}
${incident.resolution && isCurrent === false ? html`
<div class="incident-impact" style="margin-top: 12px;">
<strong>Resolution:</strong> ${incident.resolution}
</div>
` : ''}
<div class="incident-actions">
<button
<button
class="subscribe-button ${this.isSubscribedToIncident(incident.id) ? 'subscribed' : ''}"
@click=${(e: Event) => {
e.stopPropagation();
@@ -729,15 +745,10 @@ export class UplStatuspageIncidents extends DeesElement {
}}
>
${this.isSubscribedToIncident(incident.id) ? html`
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6667 3.5L5.25 9.91667L2.33334 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<dees-icon .icon=${'lucide:Check'} .iconSize=${14}></dees-icon>
Subscribed to updates
` : html`
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 5.25V8.75C10.5 9.34674 10.2629 9.91903 9.84099 10.341C9.41903 10.7629 8.84674 11 8.25 11L3.75 11C3.15326 11 2.58097 10.7629 2.15901 10.341C1.73705 9.91903 1.5 9.34674 1.5 8.75V4.25C1.5 3.65326 1.73705 3.08097 2.15901 2.65901C2.58097 2.23705 3.15326 2 3.75 2H7.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 1.5H12.5M12.5 1.5V5M12.5 1.5L6 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<dees-icon .icon=${'lucide:Bell'} .iconSize=${14}></dees-icon>
Subscribe to updates
`}
</button>
@@ -764,10 +775,7 @@ export class UplStatuspageIncidents extends DeesElement {
<div class="update-message">${update.message}</div>
${update.author ? html`
<div class="update-author">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<dees-icon .icon=${'lucide:User'} .iconSize=${12}></dees-icon>
${update.author}
</div>
` : ''}
@@ -775,45 +783,32 @@ export class UplStatuspageIncidents extends DeesElement {
`;
}
private getStatusIcon(status: string): TemplateResult {
return html`<span class="status-dot" style="
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: ${status === 'resolved' ? sharedStyles.colors.status.operational :
status === 'monitoring' ? sharedStyles.colors.status.maintenance :
status === 'identified' ? sharedStyles.colors.status.degraded :
sharedStyles.colors.status.partial};
"></span>`;
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
const now = Date.now();
const diff = now - timestamp;
// Less than 1 hour ago
if (diff < 60 * 60 * 1000) {
const minutes = Math.floor(diff / (60 * 1000));
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
}
// Less than 24 hours ago
if (diff < 24 * 60 * 60 * 1000) {
const hours = Math.floor(diff / (60 * 60 * 1000));
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
}
// Less than 7 days ago
if (diff < 7 * 24 * 60 * 60 * 1000) {
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
return `${days} day${days !== 1 ? 's' : ''} ago`;
}
// Default to full date
return date.toLocaleDateString('en-US', {
month: 'short',
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
});
@@ -823,7 +818,7 @@ export class UplStatuspageIncidents extends DeesElement {
const minutes = Math.floor(milliseconds / (60 * 1000));
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
} else if (hours > 0) {
@@ -865,9 +860,9 @@ export class UplStatuspageIncidents extends DeesElement {
if (newSubscribed.has(incident.id)) {
newSubscribed.delete(incident.id);
this.dispatchEvent(new CustomEvent('incidentUnsubscribe', {
detail: {
detail: {
incident,
incidentId: incident.id
incidentId: incident.id
},
bubbles: true,
composed: true
@@ -875,7 +870,7 @@ export class UplStatuspageIncidents extends DeesElement {
} else {
newSubscribed.add(incident.id);
this.dispatchEvent(new CustomEvent('incidentSubscribe', {
detail: {
detail: {
incident,
incidentId: incident.id,
incidentTitle: incident.title,

View File

@@ -114,38 +114,27 @@ export class UplStatuspageStatsgrid extends DeesElement {
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.stat-card.status-card::before {
/* Dynamic status-based accent colors for all stat cards */
.stat-card.operational::before {
background: ${sharedStyles.colors.status.operational};
}
.stat-card.status-card.degraded::before {
.stat-card.degraded::before {
background: ${sharedStyles.colors.status.degraded};
}
.stat-card.status-card.partial_outage::before {
.stat-card.partial_outage::before {
background: ${sharedStyles.colors.status.partial};
}
.stat-card.status-card.major_outage::before {
.stat-card.major_outage::before {
background: ${sharedStyles.colors.status.major};
}
.stat-card.status-card.maintenance::before {
.stat-card.maintenance::before {
background: ${sharedStyles.colors.status.maintenance};
}
.stat-card.uptime-card::before {
background: ${sharedStyles.colors.status.operational};
}
.stat-card.response-card::before {
background: ${sharedStyles.colors.status.maintenance};
}
.stat-card.incident-card::before {
background: ${sharedStyles.colors.status.partial};
}
.stat-card:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
@@ -391,7 +380,7 @@ export class UplStatuspageStatsgrid extends DeesElement {
</div>
` : html`
<div class="stats-grid">
<div class="stat-card status-card ${this.currentStatus}">
<div class="stat-card ${this.currentStatus}">
<div class="stat-label">
<span class="status-indicator ${this.currentStatus}"></span>
Current Status
@@ -401,7 +390,7 @@ export class UplStatuspageStatsgrid extends DeesElement {
</div>
</div>
<div class="stat-card uptime-card">
<div class="stat-card ${this.getUptimeCardStatus()}">
<div class="stat-label">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
@@ -416,7 +405,7 @@ export class UplStatuspageStatsgrid extends DeesElement {
</div>
</div>
<div class="stat-card response-card">
<div class="stat-card ${this.getResponseCardStatus()}">
<div class="stat-label">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
@@ -430,7 +419,7 @@ export class UplStatuspageStatsgrid extends DeesElement {
${this.renderResponseChange()}
</div>
<div class="stat-card incident-card">
<div class="stat-card ${this.getIncidentCardStatus()}">
<div class="stat-label">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
@@ -442,8 +431,8 @@ export class UplStatuspageStatsgrid extends DeesElement {
<div class="stat-value">
${this.totalIncidents}
</div>
<div class="stat-change neutral">
${this.affectedServices} of ${this.totalServices} services
<div class="stat-change ${this.affectedServices === 0 ? 'positive' : 'negative'}">
${this.affectedServices === 0 ? 'All services ok.' : `${this.affectedServices} of ${this.totalServices} services affected`}
</div>
</div>
</div>
@@ -467,7 +456,7 @@ export class UplStatuspageStatsgrid extends DeesElement {
// This could be enhanced with actual trend data
const trend = this.avgResponseTime < 200 ? 'positive' : this.avgResponseTime > 500 ? 'negative' : 'neutral';
const icon = trend === 'positive' ? '↓' : trend === 'negative' ? '↑' : '→';
return html`
<div class="stat-change ${trend}">
<span>${icon}</span>
@@ -475,4 +464,23 @@ export class UplStatuspageStatsgrid extends DeesElement {
</div>
`;
}
private getUptimeCardStatus(): string {
if (this.uptime >= 99.9) return 'operational';
if (this.uptime >= 99.0) return 'degraded';
return 'partial_outage';
}
private getResponseCardStatus(): string {
if (this.avgResponseTime < 200) return 'operational';
if (this.avgResponseTime < 500) return 'degraded';
return 'partial_outage';
}
private getIncidentCardStatus(): string {
if (this.affectedServices === 0) return 'operational';
if (this.currentStatus === 'major_outage') return 'major_outage';
if (this.currentStatus === 'partial_outage') return 'partial_outage';
return 'degraded';
}
}