From 2e942df5224c6153465e65d2598e28962ec8e04a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 24 Dec 2025 10:12:29 +0000 Subject: [PATCH] feat(statuspage-incidents): Add status icons/labels and revamp incident header layout; replace left border with internal severity bar and clean up formatting --- changelog.md | 8 + ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/upl-statuspage-incidents.ts | 364 ++++++++++---------- 3 files changed, 189 insertions(+), 185 deletions(-) diff --git a/changelog.md b/changelog.md index 6c17c25..8f16741 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 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 diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index d0f5f3f..cc9684b 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@uptime.link/statuspage', - version: '1.3.1', + version: '1.4.0', description: 'A catalog of web components for the UptimeLink dashboard.' } diff --git a/ts_web/elements/upl-statuspage-incidents.ts b/ts_web/elements/upl-statuspage-incidents.ts index 4df28ae..59d0c0a 100644 --- a/ts_web/elements/upl-statuspage-incidents.ts +++ b/ts_web/elements/upl-statuspage-incidents.ts @@ -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 = new Set(); + private statusIcons: Record = { + investigating: 'lucide:Search', + identified: 'lucide:Target', + monitoring: 'lucide:Eye', + resolved: 'lucide:CheckCircle', + postmortem: 'lucide:FileText', + }; + + private statusLabels: Record = { + 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)}; } @@ -362,8 +419,6 @@ export class UplStatuspageIncidents extends DeesElement { padding-bottom: 0; } - /* Vertical connector line from each dot to the next */ - /* Dot: left -22px, width 12px + border 2px*2 = 16px total, center at -14px */ .update-item:not(:last-child)::after { content: ''; position: absolute; @@ -374,7 +429,6 @@ export class UplStatuspageIncidents extends DeesElement { background: ${sharedStyles.colors.border.default}; } - /* Timeline dot */ .update-item::before { content: ''; position: absolute; @@ -528,39 +582,23 @@ 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 { @@ -573,7 +611,6 @@ export class UplStatuspageIncidents extends DeesElement { height: 10px; } - /* Mobile dot: left -18px, width 10px + border 2px*2 = 14px, center at -11px */ .update-item:not(:last-child)::after { left: -12px; top: 16px; @@ -593,16 +630,16 @@ export class UplStatuspageIncidents extends DeesElement { ` : html`
No incidents ongoing.
` } - + Past Incidents ${this.loading ? html`
- ` : this.pastIncidents.length ? + ` : this.pastIncidents.length ? this.pastIncidents.slice(0, 5).map(incident => this.renderIncident(incident, false)) : html`
No past incidents in the last ${this.daysToShow} days.
` } - + ${this.pastIncidents.length > 5 && !this.loading ? html`
- - ${this.expandedIncidents.has(incident.id) ? html` + + ${isExpanded ? html`
-
- Impact: ${incident.impact} -
- + ${incident.impact ? html` +
+ Impact: ${incident.impact} +
+ ` : ''} + ${incident.affectedServices.length > 0 ? html`
Affected Services:
@@ -697,7 +714,7 @@ export class UplStatuspageIncidents extends DeesElement { `)}
` : ''} - + ${incident.updates.length > 0 ? html`

Updates

@@ -706,21 +723,21 @@ export class UplStatuspageIncidents extends DeesElement {
` : ''} - + ${incident.rootCause && isCurrent === false ? html`
Root Cause: ${incident.rootCause}
` : ''} - + ${incident.resolution && isCurrent === false ? html`
Resolution: ${incident.resolution}
` : ''} - +
- @@ -763,10 +775,7 @@ export class UplStatuspageIncidents extends DeesElement {
${update.message}
${update.author ? html`
- - - - + ${update.author}
` : ''} @@ -774,45 +783,32 @@ export class UplStatuspageIncidents extends DeesElement { `; } - private getStatusIcon(status: string): TemplateResult { - return html``; - } - 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 }); @@ -822,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) { @@ -864,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 @@ -874,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,