From 8827a1ba38b567fce8e010c6a34f7e18a5b8fa50 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 24 Dec 2025 15:29:15 +0000 Subject: [PATCH] feat(monitor): add extended monitor statuses, check configuration, status overrides/paused indicators, and incident update templates --- changelog.md | 10 + ts_web/00_commitinfo_data.ts | 2 +- .../upladmin-incident-update.ts | 39 +- .../upladmin-monitor-form.ts | 532 +++++++++++------- .../upladmin-monitor-list.ts | 78 ++- ts_web/interfaces/index.ts | 46 +- 6 files changed, 481 insertions(+), 226 deletions(-) diff --git a/changelog.md b/changelog.md index 259c894..42684e0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2025-12-24 - 1.1.0 - feat(monitor) +add extended monitor statuses, check configuration, status overrides/paused indicators, and incident update templates + +- Extend TStatusType with new statuses: initializing, error, paused. +- Add statusMode, manualStatus, paused, checkType, checkConfig and intervalMs to service and monitor interfaces. +- Update monitor list UI to show manual-override and paused indicators, new status badges, and include new statuses in status filter. +- Add quick templates to incident update form that prefill both status and message; update applyTemplate accordingly. +- Enhance monitor form to support checkType/ICheckConfig, statusMode selection, pause flag, interval options and additional validation (domain & PageRank search term). +- Add styles and icons for new statuses and status indicator badges. + ## 2025-12-24 - 1.0.3 - fix(catalog_admin) no changes detected, no release required diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index ccf6640..12e3bb8 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-admin', - version: '1.0.3', + version: '1.1.0', description: 'Admin components for managing UptimeLink status pages, monitors, and incidents.' } diff --git a/ts_web/elements/upladmin-incident-update/upladmin-incident-update.ts b/ts_web/elements/upladmin-incident-update/upladmin-incident-update.ts index 3d650f2..c8b93ff 100644 --- a/ts_web/elements/upladmin-incident-update/upladmin-incident-update.ts +++ b/ts_web/elements/upladmin-incident-update/upladmin-incident-update.ts @@ -308,11 +308,12 @@ export class UpladminIncidentUpdate extends DeesElement { { value: 'postmortem', label: 'Postmortem', desc: 'Analysis complete' }, ]; - const templates: Array<{ icon: string; label: string; message: string }> = [ - { icon: 'lucide:Search', label: 'Started investigating', message: 'We are currently investigating this issue.' }, - { icon: 'lucide:Target', label: 'Issue identified', message: 'We have identified the root cause and are working on a fix.' }, - { icon: 'lucide:Rocket', label: 'Fix deployed', message: 'A fix has been deployed. We are monitoring the results.' }, - { icon: 'lucide:CheckCircle', label: 'Resolved', message: 'This incident has been resolved. All systems are operating normally.' }, + const templates: Array<{ icon: string; label: string; status: TIncidentStatus; message: string }> = [ + { icon: 'lucide:Search', label: 'Started investigating', status: 'investigating', message: 'We are currently investigating this issue.' }, + { icon: 'lucide:Target', label: 'Issue identified', status: 'identified', message: 'We have identified the root cause and are working on a fix.' }, + { icon: 'lucide:Rocket', label: 'Fix deployed', status: 'monitoring', message: 'A fix has been deployed. We are monitoring the results.' }, + { icon: 'lucide:CheckCircle', label: 'Resolved', status: 'resolved', message: 'This incident has been resolved. All systems are operating normally.' }, + { icon: 'lucide:FileText', label: 'Postmortem', status: 'postmortem', message: 'Postmortem will be released shortly.' }, ]; const severityIcons: Record = { @@ -340,6 +341,19 @@ export class UpladminIncidentUpdate extends DeesElement {
+
+ +
Select a template to prefill status and message:
+
+ ${templates.map(tpl => html` + + `)} +
+
+
@@ -364,17 +378,6 @@ export class UpladminIncidentUpdate extends DeesElement {
-
-
Quick templates:
-
- ${templates.map(tpl => html` - - `)} -
-
= {}; - private statusIcons: Record = { - operational: 'lucide:CheckCircle', - degraded: 'lucide:AlertTriangle', - partial_outage: 'lucide:AlertOctagon', - major_outage: 'lucide:XCircle', - maintenance: 'lucide:Wrench', + private checkTypeLabels: Record = { + assumption: 'Assumption', + function: 'Function', + pwa: 'PWA', + pagerank: 'PageRank', }; + private intervalOptions = [ + { value: 60000, label: '1 min' }, + { value: 300000, label: '5 min' }, + { value: 900000, label: '15 min' }, + { value: 1800000, label: '30 min' }, + { value: 3600000, label: '1 hour' }, + ]; + public static styles = [ plugins.domtools.elementBasic.staticStyles, sharedStyles.commonStyles, css` :host { display: block; - font-family: ${unsafeCSS(sharedStyles.fonts.base)}; } .form-container { - background: ${sharedStyles.colors.background.secondary}; - border: 1px solid ${sharedStyles.colors.border.default}; - border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)}; + background: ${cssManager.bdTheme('#ffffff', '#09090b')}; + border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; + border-radius: 12px; overflow: hidden; } .form-header { display: flex; align-items: center; - gap: ${unsafeCSS(sharedStyles.spacing.md)}; - padding: ${unsafeCSS(sharedStyles.spacing.lg)}; - border-bottom: 1px solid ${sharedStyles.colors.border.default}; - background: ${sharedStyles.colors.background.muted}; + gap: 12px; + padding: 20px 24px; + border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; } .form-header dees-icon { --icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } - .form-title-wrapper { - flex: 1; - } - .form-title { font-size: 18px; font-weight: 600; - color: ${sharedStyles.colors.text.primary}; + color: ${cssManager.bdTheme('#09090b', '#fafafa')}; margin: 0; } .form-subtitle { - font-size: 13px; - color: ${sharedStyles.colors.text.muted}; - margin-top: 4px; + font-size: 14px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + margin: 4px 0 0 0; } .form-body { - display: grid; - gap: ${unsafeCSS(sharedStyles.spacing.lg)}; - padding: ${unsafeCSS(sharedStyles.spacing.lg)}; - } - - dees-form { - display: contents; + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; } .form-row { display: grid; grid-template-columns: 1fr 1fr; - gap: ${unsafeCSS(sharedStyles.spacing.md)}; + gap: 16px; } @media (max-width: 600px) { @@ -127,85 +127,58 @@ export class UpladminMonitorForm extends DeesElement { } } + .form-section { + display: flex; + flex-direction: column; + gap: 16px; + } + + .section-label { + font-size: 14px; + font-weight: 500; + color: ${cssManager.bdTheme('#09090b', '#fafafa')}; + margin: 0; + } + + .config-box { + background: ${cssManager.bdTheme('#fafafa', '#18181b')}; + border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; + } + + .editor-container { + border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; + border-radius: 8px; + overflow: hidden; + } + + .editor-container dees-editor { + height: 180px; + display: block; + } + + .editor-hint { + font-size: 12px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + margin-top: 8px; + } + .form-actions { display: flex; justify-content: flex-end; - gap: ${unsafeCSS(sharedStyles.spacing.sm)}; - padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)}; - border-top: 1px solid ${sharedStyles.colors.border.default}; - background: ${sharedStyles.colors.background.muted}; + gap: 8px; + padding: 16px 24px; + border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; + background: ${cssManager.bdTheme('#fafafa', '#18181b')}; } - .status-section { - margin-top: ${unsafeCSS(sharedStyles.spacing.sm)}; - } - - .status-options { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: ${unsafeCSS(sharedStyles.spacing.sm)}; - } - - .status-option { + .search-engines { display: flex; - align-items: center; - gap: 12px; - padding: 14px 16px; - background: ${sharedStyles.colors.background.primary}; - border: 2px solid ${sharedStyles.colors.border.default}; - border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; - cursor: pointer; - transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)}; - } - - .status-option:hover { - border-color: ${sharedStyles.colors.border.strong}; - background: ${sharedStyles.colors.background.muted}; - } - - .status-option.selected { - border-color: ${sharedStyles.colors.accent.primary}; - background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')}; - } - - .status-option input { - display: none; - } - - .status-option dees-icon { - flex-shrink: 0; - } - - .status-option.operational dees-icon { --icon-color: ${sharedStyles.colors.status.operational}; } - .status-option.degraded dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; } - .status-option.partial_outage dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; } - .status-option.major_outage dees-icon { --icon-color: ${sharedStyles.colors.status.majorOutage}; } - .status-option.maintenance dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; } - - .status-label { - font-size: 14px; - font-weight: 500; - color: ${sharedStyles.colors.text.primary}; - } - - .field-label { - display: block; - font-size: 13px; - font-weight: 500; - color: ${sharedStyles.colors.text.primary}; - margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)}; - } - - .field-label.required::after { - content: ' *'; - color: ${sharedStyles.colors.accent.danger}; - } - - /* Style dees-input components */ - dees-input-text, - dees-input-dropdown { - --dees-input-background: ${sharedStyles.colors.background.primary}; - --dees-input-border-color: ${sharedStyles.colors.border.default}; + gap: 16px; } ` ]; @@ -225,113 +198,140 @@ export class UpladminMonitorForm extends DeesElement { public render(): TemplateResult { const isEdit = !!this.monitor?.id; - const statusOptions: Array<{ value: TStatusType; label: string }> = [ - { value: 'operational', label: 'Operational' }, - { value: 'degraded', label: 'Degraded' }, - { value: 'partial_outage', label: 'Partial Outage' }, - { value: 'major_outage', label: 'Major Outage' }, - { value: 'maintenance', label: 'Maintenance' }, - ]; const categoryOptions = this.categories.map(cat => ({ key: cat, option: cat, payload: null })); const dependencyOptions = this.availableMonitors .filter(m => m.id !== this.monitor?.id) .map(m => ({ key: m.id, option: m.displayName || m.name, payload: null })); + const intervalOptions = this.intervalOptions.map(opt => ({ key: String(opt.value), option: opt.label, payload: null })); return html`
-
+

${isEdit ? 'Edit Monitor' : 'Create Monitor'}

-

- ${isEdit ? 'Update the monitor configuration' : 'Add a new service to monitor'} -

+

${isEdit ? 'Update monitor configuration' : 'Add a new service to monitor'}

+
this.updateField('name', e.detail.value)} > this.updateField('displayName', e.detail.value)} >
this.updateField('description', e.detail.value)} >
this.updateField('category', e.detail)} > this.updateField('dependencies', Array.isArray(e.detail) ? e.detail : [e.detail])} >
-
- -
- ${statusOptions.map(opt => html` - - `)} + +
+ this.handleCheckTypeChange(e.detail.selectedOption)} + > +
+ + +
+
+ ${this.renderCheckConfigFields()}
+ + + this.updateField('intervalMs', parseInt(e.detail))} + > + + + this.updateField('paused', e.detail.value)} + > + + + ${isEdit ? html` +
+ this.handleStatusModeChange(e.detail.selectedOption)} + > + + ${this.formData.statusMode === 'manual' ? html` + this.updateField('manualStatus', e.detail.selectedOption)} + > + ` : ''} +
+ ` : ''}
- + Cancel - + ${this.loading ? html`` : ''} ${isEdit ? 'Update Monitor' : 'Create Monitor'} @@ -340,40 +340,168 @@ export class UpladminMonitorForm extends DeesElement { `; } - private handleNameChange(e: CustomEvent) { - this.formData = { ...this.formData, name: e.detail }; - if (this.errors.name) { - this.errors = { ...this.errors, name: '' }; + private renderCheckConfigFields(): TemplateResult { + const config = this.formData.checkConfig; + + switch (this.formData.checkType) { + case 'assumption': + return html` + this.updateCheckConfig('domain', e.detail.value)} + > +
+ this.updateCheckConfig('expectedStatusCode', e.detail.value)} + > + this.updateCheckConfig('expectedTitle', e.detail.value)} + > +
+ `; + + case 'function': + return html` + this.updateCheckConfig('domain', e.detail.value)} + > +
+ +
+ { + const response = await fetch(\`https://\${context.domain}\`); + return response.status === 200; +}`} + .language=${'typescript'} + .options=${{ + lineNumbers: 'on', + minimap: { enabled: false }, + scrollBeyondLastLine: false, + fontSize: 13, + tabSize: 2, + }} + @change=${(e: CustomEvent) => this.updateCheckConfig('functionDef', e.detail)} + > +
+

Return true for success, false for failure

+
+ `; + + case 'pwa': + return html` + this.updateCheckConfig('domain', e.detail.value)} + > + `; + + case 'pagerank': + return html` +
+ this.updateCheckConfig('domain', e.detail.value)} + > + this.updateCheckConfig('searchTerm', e.detail.value)} + > +
+
+ this.updateCheckConfig('checkGoogle', e.detail.value)} + > + this.updateCheckConfig('checkBing', e.detail.value)} + > +
+
+ ${config.checkGoogle !== false ? html` + this.updateCheckConfig('googleMinRank', parseInt(e.detail.value) || undefined)} + > + ` : ''} + ${config.checkBing ? html` + this.updateCheckConfig('bingMinRank', parseInt(e.detail.value) || undefined)} + > + ` : ''} +
+ `; + + default: + return html``; } } - private handleDisplayNameChange(e: CustomEvent) { - this.formData = { ...this.formData, displayName: e.detail }; - if (this.errors.displayName) { - this.errors = { ...this.errors, displayName: '' }; + private updateField(field: keyof IMonitorFormData, value: any) { + this.formData = { ...this.formData, [field]: value }; + if (this.errors[field]) { + this.errors = { ...this.errors, [field]: '' }; } } - private handleDescriptionChange(e: CustomEvent) { - this.formData = { ...this.formData, description: e.detail }; + private updateCheckConfig(field: keyof ICheckConfig, value: any) { + this.formData = { + ...this.formData, + checkConfig: { ...this.formData.checkConfig, [field]: value }, + }; } - private handleCategoryChange(e: CustomEvent) { - this.formData = { ...this.formData, category: e.detail }; + private handleCheckTypeChange(label: string) { + const checkType = (Object.keys(this.checkTypeLabels) as TCheckType[]) + .find(key => this.checkTypeLabels[key] === label) || 'assumption'; + const domain = this.formData.checkConfig?.domain || ''; + this.formData = { + ...this.formData, + checkType, + checkConfig: { domain }, + }; } - private handleDependenciesChange(e: CustomEvent) { - const selected = e.detail; - if (Array.isArray(selected)) { - this.formData = { ...this.formData, dependencies: selected }; - } else if (selected) { - // Single selection mode, convert to array - this.formData = { ...this.formData, dependencies: [selected] }; - } - } - - private handleStatusChange(status: TStatusType) { - this.formData = { ...this.formData, currentStatus: status }; + private handleStatusModeChange(label: string) { + const mode: TStatusMode = label === 'Auto' ? 'auto' : 'manual'; + this.formData = { + ...this.formData, + statusMode: mode, + manualStatus: mode === 'manual' && !this.formData.manualStatus ? 'operational' : this.formData.manualStatus, + }; } private validate(): boolean { @@ -389,6 +517,14 @@ export class UpladminMonitorForm extends DeesElement { errors.displayName = 'Display name is required'; } + if (!this.formData.checkConfig?.domain?.trim()) { + errors.domain = 'Domain is required'; + } + + if (this.formData.checkType === 'pagerank' && !this.formData.checkConfig?.searchTerm?.trim()) { + errors.searchTerm = 'Search term is required for PageRank checks'; + } + this.errors = errors; return Object.keys(errors).length === 0; } @@ -419,7 +555,11 @@ export class UpladminMonitorForm extends DeesElement { description: '', category: '', dependencies: [], - currentStatus: 'operational', + statusMode: 'auto', + paused: false, + checkType: 'assumption', + checkConfig: { domain: '' }, + intervalMs: 300000, }; this.errors = {}; } diff --git a/ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts b/ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts index 0323546..9ebf029 100644 --- a/ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts +++ b/ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts @@ -11,7 +11,7 @@ import { state, } from '@design.estate/dees-element'; import * as sharedStyles from '../../styles/shared.styles.js'; -import type { IServiceStatus } from '../../interfaces/index.js'; +import type { IServiceStatus, TStatusType } from '../../interfaces/index.js'; import { demoFunc } from './upladmin-monitor-list.demo.js'; import type { Column, ITableAction, DeesTable } from '@design.estate/dees-catalog'; @@ -21,8 +21,6 @@ declare global { } } -type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance'; - @customElement('upladmin-monitor-list') export class UpladminMonitorList extends DeesElement { public static demo = demoFunc; @@ -45,6 +43,9 @@ export class UpladminMonitorList extends DeesElement { partial_outage: 'lucide:AlertOctagon', major_outage: 'lucide:XCircle', maintenance: 'lucide:Wrench', + initializing: 'lucide:Loader', + error: 'lucide:AlertCircle', + paused: 'lucide:PauseCircle', }; private statusLabels: Record = { @@ -53,6 +54,9 @@ export class UpladminMonitorList extends DeesElement { partial_outage: 'Partial Outage', major_outage: 'Major Outage', maintenance: 'Maintenance', + initializing: 'Initializing', + error: 'Monitor Error', + paused: 'Paused', }; public static styles = [ @@ -183,6 +187,51 @@ export class UpladminMonitorList extends DeesElement { --icon-color: ${sharedStyles.colors.status.maintenance}; } + .status-badge.initializing { + background: ${cssManager.bdTheme('rgba(107, 114, 128, 0.1)', 'rgba(107, 114, 128, 0.15)')}; + color: #6b7280; + --icon-color: #6b7280; + } + + .status-badge.error { + background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(220, 38, 38, 0.15)')}; + color: #dc2626; + --icon-color: #dc2626; + } + + .status-badge.paused { + background: ${cssManager.bdTheme('rgba(139, 92, 246, 0.1)', 'rgba(139, 92, 246, 0.15)')}; + color: #8b5cf6; + --icon-color: #8b5cf6; + } + + /* Status indicators for override and pause */ + .status-cell { + display: flex; + align-items: center; + gap: 8px; + } + + .status-indicator { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 4px; + font-size: 12px; + } + + .status-indicator.override { + background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.15)', 'rgba(234, 179, 8, 0.2)')}; + --icon-color: #eab308; + } + + .status-indicator.paused { + background: ${cssManager.bdTheme('rgba(139, 92, 246, 0.15)', 'rgba(139, 92, 246, 0.2)')}; + --icon-color: #8b5cf6; + } + .monitor-info { display: flex; flex-direction: column; @@ -316,10 +365,22 @@ export class UpladminMonitorList extends DeesElement { header: 'Status', sortable: true, renderer: (value, item) => html` - - - ${this.statusLabels[item.currentStatus]} - +
+ + + ${this.statusLabels[item.currentStatus]} + + ${item.statusMode === 'manual' ? html` + + + + ` : ''} + ${item.paused && item.currentStatus !== 'paused' ? html` + + + + ` : ''} +
`, }, { @@ -391,6 +452,9 @@ export class UpladminMonitorList extends DeesElement { + + + ${this.categories.length > 0 ? html` diff --git a/ts_web/interfaces/index.ts b/ts_web/interfaces/index.ts index 141aa95..21e0e40 100644 --- a/ts_web/interfaces/index.ts +++ b/ts_web/interfaces/index.ts @@ -1,10 +1,33 @@ // Re-export interfaces from the public catalog for consistency + +// Status types +export type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance' | 'initializing' | 'error' | 'paused'; +export type TCheckType = 'assumption' | 'function' | 'pwa' | 'pagerank'; +export type TStatusMode = 'auto' | 'manual'; + +// Check configuration interface +export interface ICheckConfig { + domain: string; + // Assumption check fields + expectedTitle?: string; + expectedStatusCode?: string; + expectedDescription?: string; + // Function check fields + functionDef?: string; + // PageRank check fields + searchTerm?: string; + checkBing?: boolean; + checkGoogle?: boolean; + bingMinRank?: number; + googleMinRank?: number; +} + export interface IServiceStatus { id: string; name: string; displayName: string; description?: string; - currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance'; + currentStatus: TStatusType; lastChecked: number; uptime30d: number; uptime90d: number; @@ -12,11 +35,19 @@ export interface IServiceStatus { category?: string; dependencies?: string[]; selected?: boolean; + // Status management + statusMode?: TStatusMode; + manualStatus?: TStatusType; + paused?: boolean; + // Check configuration + checkType?: TCheckType; + checkConfig?: ICheckConfig; + intervalMs?: number; } export interface IStatusHistoryPoint { timestamp: number; - status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance'; + status: TStatusType; responseTime?: number; errorRate?: number; } @@ -44,7 +75,7 @@ export interface IIncidentDetails { } export interface IOverallStatus { - status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance'; + status: TStatusType; message: string; lastUpdated: number; affectedServices: number; @@ -77,7 +108,14 @@ export interface IMonitorFormData { description?: string; category?: string; dependencies?: string[]; - currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance'; + // Status management + statusMode: TStatusMode; + manualStatus?: TStatusType; + paused: boolean; + // Check configuration + checkType: TCheckType; + checkConfig: ICheckConfig; + intervalMs: number; } export interface IIncidentFormData {