diff --git a/.gitignore b/.gitignore index 991d42f..a3ed5d0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,3 @@ Thumbs.db # custom config.json cache.json -uptime.json diff --git a/angular.json b/angular.json index 23b55b0..a458a21 100644 --- a/angular.json +++ b/angular.json @@ -17,14 +17,12 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { - "allowedCommonJsDependencies": ["dayjs/locale/de"], "outputPath": "dist/universal-statuspage/browser", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "aot": true, - "outputHashing": "media", "assets": [ "src/favicon.png", "src/favicon-operational.ico", diff --git a/config.json b/config.json index d781b11..d988f5c 100644 --- a/config.json +++ b/config.json @@ -2,44 +2,22 @@ "authToken": "test", "title": "sp-status", "description": "Services hosted by sp-codes", - "translations": { - "de": { - "title": "sp-status", - "description": "Services von sp-codes bereitgestellt" - } - }, "servicesPath": "$.alerts.*", "idPath": "$.labels.status_service", "statePath": "$.status", "stateValues": { "operational": ["ok", "resolved"], - "maintenance": ["maintenance" ,"paused"] + "maintenance": ["paused"] }, "groups": [ { - "id": "group", - "name": "My Group", + "id": "test", + "name": "Test", "url": "http://sp-codes.de", "services": [ { "id": "test", - "name": "My Service", - "url": "http://sp-codes.de", - "statePath": "$.state" - }, { - "id": "test3", - "name": "Test3", - "statePath": "$.state" - } - ] - }, { - "id": "group2", - "name": "Group2", - "services": [ - { - "id": "test2", - "name": "Test2", - "url": "http://sp-codes.de", + "name": "test", "statePath": "$.state" } ] diff --git a/package.json b/package.json index ee80acc..960ac6e 100644 --- a/package.json +++ b/package.json @@ -15,40 +15,32 @@ }, "private": true, "dependencies": { - "@angular/animations": "~11.0.7", - "@angular/cdk": "^11.0.3", - "@angular/common": "~11.0.7", - "@angular/compiler": "~11.0.7", - "@angular/core": "~11.0.7", - "@angular/forms": "~11.0.7", - "@angular/material": "^11.0.3", - "@angular/platform-browser": "~11.0.7", - "@angular/platform-browser-dynamic": "~11.0.7", - "@angular/platform-server": "~11.0.7", - "@angular/router": "~11.0.7", + "@angular/animations": "~11.0.2", + "@angular/cdk": "^11.0.1", + "@angular/common": "~11.0.2", + "@angular/compiler": "~11.0.2", + "@angular/core": "~11.0.2", + "@angular/forms": "~11.0.2", + "@angular/material": "^11.0.1", + "@angular/platform-browser": "~11.0.2", + "@angular/platform-browser-dynamic": "~11.0.2", + "@angular/platform-server": "~11.0.2", + "@angular/router": "~11.0.2", "@fortawesome/fontawesome-free": "^5.15.1", "@nguniversal/express-engine": "^11.0.1", - "@ngx-translate/core": "^13.0.0", - "@ngx-translate/http-loader": "^6.0.0", - "@types/node-cron": "^2.0.3", "bootstrap": "^4.5.3", - "cron": "^1.8.2", - "dayjs": "^1.10.2", "express": "^4.17.1", - "flag-icon-css": "^3.5.0", "jsonpath-plus": "^4.0.0", - "node-cron": "^2.0.3", "roboto-fontface": "^0.10.0", "rxjs": "~6.6.3", "tslib": "^2.0.0", - "tz-offset": "0.0.2", "zone.js": "~0.10.2" }, "devDependencies": { - "@angular-devkit/build-angular": "~0.1100.6", - "@angular/cli": "~11.0.6", - "@angular/compiler-cli": "~11.0.7", - "@angular/language-service": "~11.0.7", + "@angular-devkit/build-angular": "~0.1100.2", + "@angular/cli": "~11.0.2", + "@angular/compiler-cli": "~11.0.2", + "@angular/language-service": "~11.0.2", "@nguniversal/builders": "^11.0.1", "@types/express": "^4.17.9", "@types/node": "^14.0.23", diff --git a/server.ts b/server.ts index 185923d..edd57b5 100644 --- a/server.ts +++ b/server.ts @@ -1,6 +1,6 @@ import 'zone.js/dist/zone-node'; -import {ngExpressEngine, RenderOptions} from '@nguniversal/express-engine'; +import {ngExpressEngine} from '@nguniversal/express-engine'; import * as express from 'express'; import {join} from 'path'; @@ -17,26 +17,14 @@ export function app() { const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index'; // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) - // server.engine('html', ngExpressEngine({ - // bootstrap: AppServerModule, - // })); - server.engine('html', (path: string, options: Readonly, callback) => { - const engine = ngExpressEngine({ - bootstrap: AppServerModule, - providers: [ - {provide: 'REQUEST', useFactory: () => options.req, deps: []} - ] - }); - engine(path, options, callback); - }); + server.engine('html', ngExpressEngine({ + bootstrap: AppServerModule, + })); server.set('view engine', 'html'); server.set('views', distFolder); server.use('/api', api); - server.get('/favicon.ico', (req, res) => { - return res.sendStatus(404); - }); // Serve static files from /browser server.get('*.*', express.static(distFolder, { diff --git a/src/app/_data/data.ts b/src/app/_data/data.ts index 69aec05..478dd1a 100644 --- a/src/app/_data/data.ts +++ b/src/app/_data/data.ts @@ -18,31 +18,9 @@ export interface Service { name: string; url?: string; state: State; - uptime: number; } export interface MetaInfo { title: string; description: string; - translations?: { - [lang: string]: { - title: string; - description: string; - } - } -} - -export interface UptimeStatus { - hours24: number; - days7: number; - days30: number; - days90: number; - days: { - date: Date; - uptime: number; - }[]; - events: { - state: State; - date: Date; - }[]; } diff --git a/src/app/_pipe/dayjs.pipe.spec.ts b/src/app/_pipe/dayjs.pipe.spec.ts deleted file mode 100644 index e1d508f..0000000 --- a/src/app/_pipe/dayjs.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DayjsPipe } from './dayjs.pipe'; - -describe('DayjsPipe', () => { - it('create an instance', () => { - const pipe = new DayjsPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/app/_pipe/dayjs.pipe.ts b/src/app/_pipe/dayjs.pipe.ts deleted file mode 100644 index 705b106..0000000 --- a/src/app/_pipe/dayjs.pipe.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import * as dayjs from 'dayjs'; -import * as utc from 'dayjs/plugin/utc'; -import * as relativeTime from 'dayjs/plugin/relativeTime'; -import * as localizedFormat from 'dayjs/plugin/localizedFormat'; -import {TranslateService} from '@ngx-translate/core'; -import 'dayjs/locale/de'; - -dayjs.extend(utc); -dayjs.extend(relativeTime); -dayjs.extend(localizedFormat); - -@Pipe({ - name: 'dayjs', - pure: false -}) -export class DayjsPipe implements PipeTransform { - constructor(private translate: TranslateService) { - } - - transform(value: string | Date, method: string, ...args: any[]): string { - const date = dayjs.utc(value); - switch (method) { - case 'to': - const to = args[0] ? dayjs.utc(args[0]) : dayjs.utc(); - const suffix = args.length > 1 && args[1] === true; - return date.locale(this.translate.currentLang).to(to, !suffix); - case 'from': - const from = args[0] ? dayjs.utc(args[0]) : dayjs.utc(); - return date.locale(this.translate.currentLang).from(from); - case 'format': - return date.local().locale(this.translate.currentLang).format(args[0]); - } - throw new Error('please pass a method to use!'); - } -} diff --git a/src/app/_service/api.service.spec.ts b/src/app/_service/api.service.spec.ts new file mode 100644 index 0000000..c0310ae --- /dev/null +++ b/src/app/_service/api.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ApiService } from './api.service'; + +describe('ApiService', () => { + let service: ApiService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/_service/api.service.ts b/src/app/_service/api.service.ts index 83554e8..af2e0b3 100644 --- a/src/app/_service/api.service.ts +++ b/src/app/_service/api.service.ts @@ -1,9 +1,9 @@ import {Inject, Injectable, PLATFORM_ID} from '@angular/core'; -import {Observable, of} from 'rxjs'; -import {CurrentStatus, MetaInfo, UptimeStatus} from '../_data/data'; -import {HttpClient} from '@angular/common/http'; -import {environment} from '../../environments/environment'; -import {isPlatformBrowser} from '@angular/common'; +import {Observable} from "rxjs"; +import {CurrentStatus, MetaInfo} from "../_data/data"; +import {HttpClient} from "@angular/common/http"; +import {environment} from "../../environments/environment"; +import {isPlatformBrowser} from "@angular/common"; @Injectable({ providedIn: 'root' @@ -16,14 +16,10 @@ export class ApiService { } public getServiceStates(): Observable { - return this.http.get(this.api + '/status'); - } - - public getServiceUptime(id: string): Observable { - return this.http.get(this.api + '/uptime', {params: {service: id}}); + return this.http.get(this.api+ '/status'); } public getMetaInfo(): Observable { - return this.http.get(this.api + '/info'); + return this.http.get(this.api+ '/info'); } } diff --git a/src/app/_service/storage.service.ts b/src/app/_service/storage.service.ts deleted file mode 100644 index cf42735..0000000 --- a/src/app/_service/storage.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {Inject, Injectable, PLATFORM_ID} from '@angular/core'; -import {isPlatformBrowser} from '@angular/common'; - -@Injectable({ - providedIn: 'root' -}) -export class StorageService { - constructor(@Inject(PLATFORM_ID) private platformId: Object) { - } - - getValue(key: string): any { - if (!isPlatformBrowser(this.platformId)) { - return null; - } - try { - return JSON.parse(localStorage.getItem(key)); - } catch (e) { - return null; - } - } - - setValue(key: string, value: any): void { - if (isPlatformBrowser(this.platformId)) { - try { - localStorage.setItem(key, JSON.stringify(value)); - } catch (e) { - } - } - } -} diff --git a/src/app/app.component.html b/src/app/app.component.html index dad89c8..9ad4c05 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,22 +1,7 @@
-
-
-

{{translations[getLanguage()]?.title || title}}

-
-
- -
-
-

{{translations[getLanguage()]?.description || description}}

-
+

{{title}}

+

{{description}}

diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 8d7031d..238bbd2 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -19,6 +19,12 @@ footer { flex-shrink: 0; } -.language-selection { - font-size: 1.3em; +a { + color: #cccccc; + text-decoration: none; + + &:hover { + color: #ffffff; + text-decoration: underline; + } } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts new file mode 100644 index 0000000..994ccc9 --- /dev/null +++ b/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'universal-statuspage'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('universal-statuspage'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain('universal-statuspage app is running!'); + }); +}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 32afafa..83cacfb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,9 +1,8 @@ -import {Component, Inject, Injector, OnInit, PLATFORM_ID} from '@angular/core'; -import {ApiService} from './_service/api.service'; -import {Title} from '@angular/platform-browser'; -import {TranslateService} from '@ngx-translate/core'; -import {StorageService} from './_service/storage.service'; -import {isPlatformServer} from '@angular/common'; +import {Component, OnInit} from '@angular/core'; +import {ApiService} from "./_service/api.service"; +import {Observable} from "rxjs"; +import {MetaInfo} from "./_data/data"; +import {Title} from "@angular/platform-browser"; @Component({ selector: 'app-root', @@ -13,42 +12,15 @@ import {isPlatformServer} from '@angular/common'; export class AppComponent implements OnInit { title: string; description: string; - translations: { [lang: string]: { title: string; description: string } }; - private supportedLanguages = ['en', 'de']; - - constructor(private translate: TranslateService, private api: ApiService, - private storage: StorageService, private htmlTitle: Title, - private injector: Injector, @Inject(PLATFORM_ID) private platformId: Object) { - this.translate.setDefaultLang('en'); - if (isPlatformServer(platformId)) { - const request = this.injector.get( 'REQUEST'); - const requestLanguage = request.acceptsLanguages(this.supportedLanguages) || 'en'; - this.translate.use(requestLanguage); - return; - } - let language = this.storage.getValue('language') || this.translate.getBrowserLang(); - if (language ! in this.supportedLanguages) { - language = 'en'; - } - translate.use(language); + constructor(private api: ApiService, private htmlTitle: Title) { } ngOnInit(): void { this.api.getMetaInfo().subscribe(info => { this.title = info.title; this.description = info.description; - this.translations = info.translations; this.htmlTitle.setTitle(this.title); - }); - } - - getLanguage(): string { - return this.translate.currentLang; - } - - setLanguage(language: string): void { - this.translate.use(language); - this.storage.setValue('language', language); + }) } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 297ecdd..db73aab 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,56 +1,26 @@ import {BrowserModule} from '@angular/platform-browser'; -import {NgModule, PLATFORM_ID} from '@angular/core'; +import {NgModule} from '@angular/core'; import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {StatusComponent} from './status/status.component'; -import {MatExpansionModule} from '@angular/material/expansion'; -import {MatListModule} from '@angular/material/list'; -import {HttpClient, HttpClientModule} from '@angular/common/http'; -import {MatTooltipModule} from '@angular/material/tooltip'; -import {UptimeComponent} from './uptime/uptime.component'; -import {DayjsPipe} from './_pipe/dayjs.pipe'; -import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; -import {TranslateHttpLoader} from '@ngx-translate/http-loader'; -import {isPlatformServer} from '@angular/common'; -import {from, Observable} from 'rxjs'; - -export class TranslateUniversalLoader extends TranslateLoader { - getTranslation(lang: string): Observable { - return from(import(`../assets/i18n/${lang}.json`)); - } -} - -export function UniversalLoaderFactory(http: HttpClient, plattformId: Object) { - if (isPlatformServer(plattformId)) { - return new TranslateUniversalLoader(); - } - return new TranslateHttpLoader(http); -} +import {MatExpansionModule} from "@angular/material/expansion"; +import {MatListModule} from "@angular/material/list"; +import {HttpClientModule} from "@angular/common/http"; @NgModule({ declarations: [ AppComponent, - StatusComponent, - UptimeComponent, - DayjsPipe + StatusComponent ], imports: [ BrowserModule.withServerTransition({appId: 'serverApp'}), AppRoutingModule, BrowserAnimationsModule, HttpClientModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: UniversalLoaderFactory, - deps: [HttpClient, PLATFORM_ID] - } - }), MatExpansionModule, - MatListModule, - MatTooltipModule + MatListModule ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/status/status.component.html b/src/app/status/status.component.html index 91ccd84..f0b5aa2 100644 --- a/src/app/status/status.component.html +++ b/src/app/status/status.component.html @@ -1,38 +1,25 @@ -
-

- - {{group.name}} - {{group.name}} -

- - - - -
- - {{service.name}} - {{service.name}} - {{service.uptime?.toFixed(2)}}% - - {{'state.' + service.state | translate}} -
-
-
+ + + + + + {{group.name}} + {{group.name}} + + - - - - - -
+ + +
+ + {{service.name}} + + {{service.state}} +
+ +
+
+ + -
{{'last-updated' | translate:{'time': lastUpdated | dayjs:'from'} }} -
+
Last updated {{lastUpdated | date:'HH:mm:ss'}}
diff --git a/src/app/status/status.component.scss b/src/app/status/status.component.scss index 19c51a6..310e4f4 100644 --- a/src/app/status/status.component.scss +++ b/src/app/status/status.component.scss @@ -1,5 +1,23 @@ +.operational { + color: #7ed321; +} + +.outage { + color: #ff6f6f; +} + +.maintenance { + color: #f7ca18; +} + a { + text-decoration: none; + outline: none; color: #ffffff; + + &:hover.name, &:hover .name { + text-decoration: underline; + } } mat-panel-title { diff --git a/src/app/status/status.component.spec.ts b/src/app/status/status.component.spec.ts new file mode 100644 index 0000000..0fed327 --- /dev/null +++ b/src/app/status/status.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { StatusComponent } from './status.component'; + +describe('StatusComponent', () => { + let component: StatusComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ StatusComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/status/status.component.ts b/src/app/status/status.component.ts index 8c64f50..44212ec 100644 --- a/src/app/status/status.component.ts +++ b/src/app/status/status.component.ts @@ -1,10 +1,11 @@ import {Component, Inject, OnDestroy, OnInit, PLATFORM_ID} from '@angular/core'; -import {ApiService} from '../_service/api.service'; -import {Group} from '../_data/data'; -import {interval, Subject} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; -import {DOCUMENT, isPlatformBrowser} from '@angular/common'; -import {StorageService} from '../_service/storage.service'; +import {ApiService} from "../_service/api.service"; +import {Group} from "../_data/data"; +import {interval, Subject} from "rxjs"; +import {flatMap, startWith, takeUntil} from "rxjs/operators"; +import {DOCUMENT, isPlatformBrowser} from "@angular/common"; + +// import {DOCUMENT} from "@angular/common"; @Component({ selector: 'app-status', @@ -13,24 +14,17 @@ import {StorageService} from '../_service/storage.service'; }) export class StatusComponent implements OnInit, OnDestroy { readonly stateClasses = { - 'operational': 'fas fa-fw fa-heart operational mr-2', - 'outage': 'fas fa-fw fa-heart-broken outage mr-2', - 'maintenance': 'fas fa-fw fa-heartbeat maintenance mr-2' + "operational": 'fas fa-fw fa-heart operational mr-2', + "outage": 'fas fa-fw fa-heart-broken outage mr-2', + "maintenance": 'fas fa-fw fa-heartbeat maintenance mr-2' }; destroyed$ = new Subject(); groups: Group[]; lastUpdated: Date; - expandedCache: { [id: string]: boolean }; - constructor(private api: ApiService, private storage: StorageService, - @Inject(PLATFORM_ID) private platformId: Object, + constructor(private api: ApiService, @Inject(PLATFORM_ID) private platformId: Object, @Inject(DOCUMENT) private document: Document) { - let cache = this.storage.getValue('expanded'); - if (typeof cache !== 'object') { - cache = null; - } - this.expandedCache = cache || {}; } ngOnInit(): void { @@ -55,8 +49,4 @@ export class StatusComponent implements OnInit, OnDestroy { this.destroyed$.next(); this.destroyed$.complete(); } - - saveExpandedCache() { - this.storage.setValue('expanded', this.expandedCache); - } } diff --git a/src/app/uptime/uptime.component.html b/src/app/uptime/uptime.component.html deleted file mode 100644 index 9266374..0000000 --- a/src/app/uptime/uptime.component.html +++ /dev/null @@ -1,70 +0,0 @@ -
-

{{'uptime.title' | translate}}

- -
-
-
-

{{uptime.hours24?.toFixed(2)}}%

- {{'uptime.last24hours' | translate}} -
-
-
-
-

{{uptime.days7?.toFixed(2)}}%

- {{'uptime.last7days' | translate}} -
-
-
-
-

{{uptime.days30?.toFixed(2)}}%

- {{'uptime.last30days' | translate}} -
-
-
-
-

{{uptime.days90?.toFixed(2)}}%

- {{'uptime.last90days' | translate}} -
-
-
-
- -
-
-
- -
-

{{'recent-events.title' | translate}}

- - - - -

- {{'recent-events.operational' | translate}} - {{'recent-events.maintenance' | translate: {'time': event.date | dayjs:'to':uptime.events[index - 1]?.date} }} - {{'recent-events.outage' | translate:{'time': event.date | dayjs:'to':uptime.events[index - 1]?.date} }} -

-
{{event.date | dayjs:'from'}} -
- -
-
-
- {{(expanded ? 'show-less' : 'show-all') | translate}} -
-
- -
{{error}}
-
{{'loading' | translate}}
-
diff --git a/src/app/uptime/uptime.component.scss b/src/app/uptime/uptime.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/uptime/uptime.component.ts b/src/app/uptime/uptime.component.ts deleted file mode 100644 index 30cd58f..0000000 --- a/src/app/uptime/uptime.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {Observable, of} from 'rxjs'; -import {UptimeStatus} from '../_data/data'; -import {ApiService} from '../_service/api.service'; -import {catchError} from 'rxjs/operators'; -import {StorageService} from '../_service/storage.service'; - -@Component({ - selector: 'app-uptime', - templateUrl: './uptime.component.html', - styleUrls: ['./uptime.component.scss'] -}) -export class UptimeComponent implements OnInit { - @Input() id: string; - readonly stateClasses = { - 'operational': 'fas fa-fw fa-heart operational mr-2', - 'outage': 'fas fa-fw fa-heart-broken outage mr-2', - 'maintenance': 'fas fa-fw fa-heartbeat maintenance mr-2' - }; - uptime$: Observable; - error: string; - expanded: boolean; - - constructor(private api: ApiService, private storage: StorageService) { - } - - ngOnInit(): void { - if (!this.id) { - throw new Error('please pass a service id!'); - } - let value = this.storage.getValue('show-events-' + this.id); - console.log(value, typeof value); - if (typeof value !== 'boolean') { - value = false; - } - this.expanded = value; - this.uptime$ = this.api.getServiceUptime(this.id) - .pipe(catchError(err => { - if (err.status === 404) { - this.error = 'No uptime information available.'; - } else { - this.error = 'An unexpected error occurred: ' + err.error; - } - return of(null); - })); - } - - toggleExpanded(): void { - this.expanded = !this.expanded; - this.storage.setValue('show-events-' + this.id, this.expanded); - } -} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json deleted file mode 100644 index 5ae699f..0000000 --- a/src/assets/i18n/de.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "state": { - "operational": "Funktionsfähig", - "maintenance": "Wartung", - "outage": "Ausfall" - }, - "uptime": { - "title": "Verfügbarkeit", - "last24hours": "Letzte 24 Stunden", - "last7days": "Letzte 7 Tage", - "last30days": "Letzte 30 Tage", - "last90days": "Letzte 90 Tage" - }, - "recent-events": { - "title": "Letzte Ereignisse", - "operational": "Wieder funktionsfähig", - "maintenance": "Wartung für {{time}}", - "outage": "Ausfall für {{time}}" - }, - "last-updated": "Aktualisiert {{time}}", - "loading": "Lade...", - "show-all": "Alle anzeigen", - "show-less": "Weniger anzeigen" -} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json deleted file mode 100644 index 9e6d0cf..0000000 --- a/src/assets/i18n/en.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "state": { - "operational": "Operational", - "maintenance": "Maintenance", - "outage": "Outage" - }, - "uptime": { - "title": "Uptime", - "last24hours": "Last 24 hours", - "last7days": "Last 7 days", - "last30days": "Last 30 days", - "last90days": "Last 90 days" - }, - "recent-events": { - "title": "Recent events", - "operational": "Operational again", - "maintenance": "Maintenance for {{time}}", - "outage": "Outage for {{time}}" - }, - "last-updated": "Last updated {{time}}", - "loading": "Loading...", - "show-all": "Show all", - "show-less": "Show less" -} diff --git a/src/main.status.ts b/src/main.status.ts index 6f6de4c..db41deb 100644 --- a/src/main.status.ts +++ b/src/main.status.ts @@ -1,16 +1,8 @@ import {json, Router} from 'express'; -import {CurrentStatus, State, UptimeStatus} from './app/_data/data'; +import {CurrentStatus, Service, State} from './app/_data/data'; import {existsSync, readFileSync, writeFileSync} from 'fs'; import {join} from 'path'; import {JSONPath} from 'jsonpath-plus'; -import * as dayjs from 'dayjs'; -import {Dayjs} from 'dayjs'; -import * as utc from 'dayjs/plugin/utc'; -import * as isBetween from 'dayjs/plugin/isBetween'; -import {CronJob} from 'cron'; - -dayjs.extend(utc); -dayjs.extend(isBetween); interface Cache { [id: string]: State; @@ -20,12 +12,6 @@ interface Config { authToken: string; title: string; description: string; - translations?: { - [lang: string]: { - title: string; - description: string; - } - }, servicesPath?: string; idPath?: string; statePath?: string; @@ -60,7 +46,6 @@ const serviceStatePaths: { [service: string]: string } = config.groups }, {}); let cache: CurrentStatus; -let uptimeStates = existsSync(join(process.cwd(), 'uptime.json')) ? JSON.parse(readFileSync(join(process.cwd(), 'uptime.json'), {encoding: 'utf-8'})) : {} as { [id: string]: UptimeStatus; }; updateCache(); api.post('/update/health', (req, res) => { @@ -75,48 +60,40 @@ api.post('/update/health', (req, res) => { } else if (config.servicesPath && config.idPath && config.statePath) { services = JSONPath({path: config.servicesPath, json: req.body}) .map(s => ({ - id: JSONPath({path: config.idPath, json: s, wrap: false}), - state: JSONPath({path: config.statePath, json: s, wrap: false}) + id: JSONPath({path: config.idPath, json: s, wrap: false}), + state: JSONPath({path: config.statePath, json: s, wrap: false}) })); } services.forEach(s => { if (config.stateValues.operational.includes(s.state)) { - updateServiceState(s.id, 'operational'); + serviceStates[s.id] = 'operational'; } else if (config.stateValues.maintenance.includes(s.state)) { - updateServiceState(s.id, 'maintenance'); + serviceStates[s.id] = 'maintenance'; } else { - updateServiceState(s.id, 'outage'); + serviceStates[s.id] = 'outage'; } }); updateCache(); - persistCache(); + + writeFileSync('cache.json', JSON.stringify(serviceStates), {encoding: 'utf-8'}); return res.send('OK'); }); api.get('/status', (req, res) => { - return res.json(cache); -}); - -api.get('/uptime', (req, res) => { - const serviceId = req.query.service as string; - const uptime = uptimeStates[serviceId]; - if (uptime) { - return res.json(uptime); - } - return res.sendStatus(404); + return res.json(cache); }); api.get('/badge', (req, res) => { const serviceId = req.query.service as string; if (!serviceId) { return res.json({ - 'schemaVersion': 1, - 'label': 'sp-status', - 'message': 'service not provided', - 'isError': true + "schemaVersion": 1, + "label": "sp-status", + "message": "service not provided", + "isError": true }); } const service = cache.groups @@ -124,10 +101,10 @@ api.get('/badge', (req, res) => { .find(s => s.id === serviceId); if (!service) { return res.json({ - 'schemaVersion': 1, - 'label': 'sp-status', - 'message': 'service not found', - 'isError': true + "schemaVersion": 1, + "label": "sp-status", + "message": "service not found", + "isError": true }); } const label = req.query.label || service.name; @@ -148,51 +125,28 @@ api.get('/badge', (req, res) => { break; } return res.json({ - 'schemaVersion': 1, - 'label': label, - 'message': message, - 'color': color + "schemaVersion": 1, + "label": label, + "message": message, + "color": color }); }); api.get('/info', (req, res) => { return res.json({ title: config.title, - description: config.description, - translations: config.translations + description: config.description }); }); -function updateServiceState(id: string, state: string) { - if (serviceStates[id] === state) { - return; - } - serviceStates[id] = state; - if (!uptimeStates[id]) { - uptimeStates[id] = { - days: [], - events: [] - }; - } - if (uptimeStates[id].events.length === 0 && state !== 'operational' || - uptimeStates[id].events.length > 0 && uptimeStates[id].events[0].state !== state) { - uptimeStates[id].events.unshift({state: state, date: new Date()}); - console.log(`${id} changed to ${state}`); - } -} - function updateCache(): void { - updateUptime(); - const groups = config.groups.map(group => { const services = group.services.map(service => { - const uptime = uptimeStates[service.id]; return { id: service.id, name: service.name, url: service.url, - state: serviceStates[service.id] || 'operational', - uptime: uptime ? uptime.days30 : 100 + state: serviceStates[service.id] || 'operational' }; }); return { @@ -209,151 +163,8 @@ function updateCache(): void { }; } -function updateUptime() { - const now = dayjs.utc(); - const today = now.startOf('d'); - const eventLimit = now.subtract(7, 'd'); - for (const id in uptimeStates) { - if (uptimeStates.hasOwnProperty(id)) { - const uptime = uptimeStates[id] as UptimeStatus; - if (uptime.days.length < 90) { - for (let i = 0; i < 90; i++) { - uptime.days.push({date: today.subtract(90 - i, 'd').toDate(), uptime: 100}) - } - } - if (today.diff(dayjs.utc(uptime.days[uptime.days.length - 1].date), 'd') >= 1) { - uptime.days.push({date: today.toDate(), uptime: 0}); - } - if (uptime.days.length > 90) { - uptime.days.splice(0, uptime.days.length - 90); - } - for (let i = uptime.days.length - 3; i < uptime.days.length; i++) { - const start = dayjs.utc(uptime.days[i].date); - let end = start.add(1, 'd'); - if (end.isAfter(now)) { - end = now; - } - uptime.days[i].uptime = calculateUptime(start, end, uptime.events); - } - uptime.hours24 = calculateUptime(now.subtract(24, 'h'), now, uptime.events); - uptime.days7 = uptime.days.slice(uptime.days.length - 7, uptime.days.length).map(e => e.uptime).reduce((a, b) => a + b) / 7; - uptime.days30 = uptime.days.slice(uptime.days.length - 30, uptime.days.length).map(e => e.uptime).reduce((a, b) => a + b) / 30; - uptime.days90 = uptime.days.slice(uptime.days.length - 90, uptime.days.length).map(e => e.uptime).reduce((a, b) => a + b) / 90; - uptime.events = uptime.events.filter(e => dayjs.utc(e.date).isAfter(eventLimit)); - if (uptime.events.length > 0 && uptime.events[uptime.events.length - 1].state === 'operational') { - uptime.events.pop(); - } - } - } -} - -function calculateUptime(start: Dayjs, end: Dayjs, events: { state: State; date: Date; }[]): number { - if (events.filter(event => dayjs.utc(event.date).isBetween(start, end)).length == 0) { - const lastEvent = events.filter(event => dayjs.utc(event.date).isBefore(start))[0]; - if (lastEvent && lastEvent.state !== 'operational') { - return 0; - } - return 100; - } - let uptimeMillis = 0; - let newestEventDate; - for (let i = events.length - 1; i >= 0; i--) { - const event = events[i]; - const eventDate = dayjs.utc(event.date); - const lastEvent = events[i + 1]; - let lastEventDate = lastEvent ? dayjs.utc(lastEvent.date) : start; - if (lastEventDate.isBefore(start)) { - lastEventDate = start; - } - if (eventDate.isBetween(start, end)) { - if (event.state === 'operational') { - newestEventDate = eventDate; - } else if (!lastEvent || lastEvent.state === 'operational') { - newestEventDate = null; - uptimeMillis += eventDate.diff(lastEventDate, 'ms'); - } - } - } - if (newestEventDate) { - uptimeMillis += end.diff(newestEventDate, 'ms'); - } - return uptimeMillis / end.diff(start, 'ms') * 100; -} - function calculateOverallState(states: State[]): State { return states.includes('outage') ? 'outage' : states.includes('maintenance') ? 'maintenance' : 'operational'; } -function persistCache() { - writeFileSync('cache.json', JSON.stringify(serviceStates), {encoding: 'utf-8'}); - writeFileSync('uptime.json', JSON.stringify(uptimeStates), {encoding: 'utf-8'}); -} - -new CronJob('0 * * * * *', () => updateCache(), null, true, 'UTC').start(); -new CronJob('0 0 * * * *', () => persistCache(), null, true, 'UTC').start(); - - -api.get('/test', (req, res) => { - return res.json({ - '50_5': calculateUptime(dayjs.utc('2020-01-02'), dayjs.utc('2020-01-03'), [{ - state: 'outage', - date: new Date('2020-01-03T12:00:00.000Z') - }, { - state: 'operational', - date: new Date('2020-01-02T18:00:00.000Z') - }, { - state: 'outage', - date: new Date('2020-01-02T06:00:00.000Z') - }, { - state: 'operational', - date: new Date('2020-01-01T12:00:00.000Z') - }]), - '50_4': calculateUptime(dayjs.utc('2020-01-02'), dayjs.utc('2020-01-03'), [{ - state: 'operational', - date: new Date('2020-01-02T18:00:00.000Z') - }, { - state: 'outage', - date: new Date('2020-01-02T06:00:00.000Z') - }, { - state: 'operational', - date: new Date('2020-01-01T12:00:00.000Z') - }]), - '50_3': calculateUptime(dayjs.utc('2020-01-02'), dayjs.utc('2020-01-03'), [{ - state: 'outage', - date: new Date('2020-01-02T12:00:00.000Z') - }, { - state: 'operational', - date: new Date('2020-01-01T12:00:00.000Z') - }]), - '50_2': calculateUptime(dayjs.utc('2020-01-02'), dayjs.utc('2020-01-03'), [{ - state: 'outage', - date: new Date('2020-01-02T18:00:00.000Z') - }, { - state: 'operational', - date: new Date('2020-01-02T06:00:00.000Z') - }]), - '50_1': calculateUptime(dayjs.utc('2020-01-02'), dayjs.utc('2020-01-03'), [{ - state: 'operational', - date: new Date('2020-01-02T12:00:00.000Z') - }, { - state: 'outage', - date: new Date('2020-01-01T12:00:00.000Z') - }]), - '50_0': calculateUptime(dayjs.utc('2020-01-01'), dayjs.utc('2020-01-02'), [{ - state: 'operational', - date: new Date('2020-01-01T12:00:00.000Z') - }]), - '75': calculateUptime(dayjs.utc('2020-01-01'), dayjs.utc('2020-01-02'), [{ - state: 'operational', - date: new Date('2020-01-01T06:00:00.000Z') - }]), - '100': calculateUptime(dayjs.utc('2020-01-01'), dayjs.utc('2020-01-02'), []), - '0': calculateUptime(dayjs.utc('2020-01-02'), dayjs.utc('2020-01-03'), [{ - state: 'outage', - date: new Date('2020-01-01T12:00:00.000Z') - }]), - 'test': calculateUptime(dayjs.utc('2020-01-07'), dayjs.utc(), [{state: 'outage', date: new Date('2021-01-07T13:54:32.705Z')}]) - }); -}); - export {api}; diff --git a/src/styles.scss b/src/styles.scss index 539e68a..066912a 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -3,17 +3,8 @@ @import "~bootstrap/scss/mixins"; @import "~bootstrap/scss/utilities"; @import "~bootstrap/scss/bootstrap-grid"; -@import "~bootstrap/scss/functions"; -@import "~bootstrap/scss/variables"; -@import "~bootstrap/scss/mixins/_breakpoints"; +@import "~@fortawesome/fontawesome-free/css/all.css"; @import "~roboto-fontface/css/roboto/roboto-fontface.css"; -$fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; -@import "~@fortawesome/fontawesome-free/scss/fontawesome"; -@import "~@fortawesome/fontawesome-free/scss/solid"; -@import "~@fortawesome/fontawesome-free/scss/brands"; -@import "~@fortawesome/fontawesome-free/scss/regular"; -$flag-icon-css-path: '~flag-icon-css/flags' !default; -@import "~flag-icon-css/sass/flag-icon"; @import '~@angular/material/theming'; @@ -44,21 +35,21 @@ $dark-dividers: rgba($dark-primary-text, 0.12); $dark-focused: rgba($dark-primary-text, 0.12); $mat-light-theme-foreground: ( - base: black, - divider: $dark-dividers, - dividers: $dark-dividers, - disabled: $dark-disabled-text, - disabled-button: rgba($dark-text, 0.26), - disabled-text: $dark-disabled-text, - elevation: black, - secondary-text: $dark-accent-text, - hint-text: $dark-disabled-text, - accent-text: $dark-accent-text, - icon: $dark-accent-text, - icons: $dark-accent-text, - text: $dark-primary-text, - slider-min: $dark-primary-text, - slider-off: rgba($dark-text, 0.26), + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba($dark-text, 0.26), + disabled-text: $dark-disabled-text, + elevation: black, + secondary-text: $dark-accent-text, + hint-text: $dark-disabled-text, + accent-text: $dark-accent-text, + icon: $dark-accent-text, + icons: $dark-accent-text, + text: $dark-primary-text, + slider-min: $dark-primary-text, + slider-off: rgba($dark-text, 0.26), slider-off-active: $dark-disabled-text, ); @@ -71,80 +62,80 @@ $light-dividers: rgba($light-primary-text, 0.12); $light-focused: rgba($light-primary-text, 0.12); $mat-dark-theme-foreground: ( - base: $light-text, - divider: $light-dividers, - dividers: $light-dividers, - disabled: $light-disabled-text, - disabled-button: rgba($light-text, 0.3), - disabled-text: $light-disabled-text, - elevation: black, - hint-text: $light-disabled-text, - secondary-text: $light-accent-text, - accent-text: $light-accent-text, - icon: $light-text, - icons: $light-text, - text: $light-text, - slider-min: $light-text, - slider-off: rgba($light-text, 0.3), + base: $light-text, + divider: $light-dividers, + dividers: $light-dividers, + disabled: $light-disabled-text, + disabled-button: rgba($light-text, 0.3), + disabled-text: $light-disabled-text, + elevation: black, + hint-text: $light-disabled-text, + secondary-text: $light-accent-text, + accent-text: $light-accent-text, + icon: $light-text, + icons: $light-text, + text: $light-text, + slider-min: $light-text, + slider-off: rgba($light-text, 0.3), slider-off-active: rgba($light-text, 0.3), ); // Background config // Light bg -$light-background: #fafafa; -$light-bg-darker-5: darken($light-background, 5%); -$light-bg-darker-10: darken($light-background, 10%); -$light-bg-darker-20: darken($light-background, 20%); -$light-bg-darker-30: darken($light-background, 30%); -$light-bg-lighter-5: lighten($light-background, 5%); -$dark-bg-tooltip: lighten(#2c2c2c, 20%); -$dark-bg-alpha-4: rgba(#2c2c2c, 0.04); -$dark-bg-alpha-12: rgba(#2c2c2c, 0.12); +$light-background: #fafafa; +$light-bg-darker-5: darken($light-background, 5%); +$light-bg-darker-10: darken($light-background, 10%); +$light-bg-darker-20: darken($light-background, 20%); +$light-bg-darker-30: darken($light-background, 30%); +$light-bg-lighter-5: lighten($light-background, 5%); +$dark-bg-tooltip: lighten(#2c2c2c, 20%); +$dark-bg-alpha-4: rgba(#2c2c2c, 0.04); +$dark-bg-alpha-12: rgba(#2c2c2c, 0.12); $mat-light-theme-background: ( - background: $light-background, - status-bar: $light-bg-darker-20, - app-bar: $light-bg-darker-5, - hover: $dark-bg-alpha-4, - card: $light-bg-lighter-5, - dialog: $light-bg-lighter-5, - tooltip: $dark-bg-tooltip, - disabled-button: $dark-bg-alpha-12, - raised-button: $light-bg-lighter-5, - focused-button: $dark-focused, - selected-button: $light-bg-darker-20, + background: $light-background, + status-bar: $light-bg-darker-20, + app-bar: $light-bg-darker-5, + hover: $dark-bg-alpha-4, + card: $light-bg-lighter-5, + dialog: $light-bg-lighter-5, + tooltip: $dark-bg-tooltip, + disabled-button: $dark-bg-alpha-12, + raised-button: $light-bg-lighter-5, + focused-button: $dark-focused, + selected-button: $light-bg-darker-20, selected-disabled-button: $light-bg-darker-30, - disabled-button-toggle: $light-bg-darker-10, - unselected-chip: $light-bg-darker-10, - disabled-list-option: $light-bg-darker-10, + disabled-button-toggle: $light-bg-darker-10, + unselected-chip: $light-bg-darker-10, + disabled-list-option: $light-bg-darker-10, ); // Dark bg -$dark-background: #2c2c2c; -$dark-bg-lighter-5: lighten($dark-background, 5%); -$dark-bg-lighter-10: lighten($dark-background, 10%); -$dark-bg-lighter-20: lighten($dark-background, 20%); -$dark-bg-lighter-30: lighten($dark-background, 30%); -$light-bg-alpha-4: rgba(#fafafa, 0.04); -$light-bg-alpha-12: rgba(#fafafa, 0.12); +$dark-background: #2c2c2c; +$dark-bg-lighter-5: lighten($dark-background, 5%); +$dark-bg-lighter-10: lighten($dark-background, 10%); +$dark-bg-lighter-20: lighten($dark-background, 20%); +$dark-bg-lighter-30: lighten($dark-background, 30%); +$light-bg-alpha-4: rgba(#fafafa, 0.04); +$light-bg-alpha-12: rgba(#fafafa, 0.12); // Background palette for dark themes. $mat-dark-theme-background: ( - background: $dark-background, - status-bar: $dark-bg-lighter-20, - app-bar: $dark-bg-lighter-5, - hover: $light-bg-alpha-4, - card: $dark-bg-lighter-5, - dialog: $dark-bg-lighter-5, - tooltip: $dark-bg-lighter-20, - disabled-button: $light-bg-alpha-12, - raised-button: $dark-bg-lighter-5, - focused-button: $light-focused, - selected-button: $dark-bg-lighter-20, + background: $dark-background, + status-bar: $dark-bg-lighter-20, + app-bar: $dark-bg-lighter-5, + hover: $light-bg-alpha-4, + card: $dark-bg-lighter-5, + dialog: $dark-bg-lighter-5, + tooltip: $dark-bg-lighter-20, + disabled-button: $light-bg-alpha-12, + raised-button: $dark-bg-lighter-5, + focused-button: $light-focused, + selected-button: $dark-bg-lighter-20, selected-disabled-button: $dark-bg-lighter-30, - disabled-button-toggle: $dark-bg-lighter-10, - unselected-chip: $dark-bg-lighter-20, - disabled-list-option: $dark-bg-lighter-10, + disabled-button-toggle: $dark-bg-lighter-10, + unselected-chip: $dark-bg-lighter-20, + disabled-list-option: $dark-bg-lighter-10, ); // Compute font config @@ -216,8 +207,7 @@ $mat-warn: ( darker: $light-primary-text, ) ); -$theme-warn: mat-palette($mat-warn, main, lighter, darker); -; +$theme-warn: mat-palette($mat-warn, main, lighter, darker);; $theme: mat-dark-theme($theme-primary, $theme-accent, $theme-warn); $altTheme: mat-light-theme($theme-primary, $theme-accent, $theme-warn); @@ -254,47 +244,3 @@ body { background-color: #222222; color: #ffffff; } - -.multiline-tooltip { - white-space: pre-line; -} - -.operational { - color: #7ed321; -} - -.outage { - color: #ff6f6f; -} - -.maintenance { - color: #f7ca18; -} - -.bg-operational { - background-color: #7ed321; -} - -.bg-outage { - background-color: #ff6f6f; -} - -.bg-maintenance { - background-color: #f7ca18; -} - -a { - color: #cccccc; - text-decoration: none; - - &:hover { - color: #ffffff; - text-decoration: underline; - } -} - -@include media-breakpoint-up(md) { - .border-md-right { - border-right: $border-width solid $border-color !important; - } -}