52 changed files with 2902 additions and 364 deletions
@ -1,3 +1,51 @@
|
||||
.idea/ |
||||
*.iml |
||||
# See http://help.github.com/ignore-files/ for more about ignoring files. |
||||
|
||||
# compiled output |
||||
/dist |
||||
/tmp |
||||
/out-tsc |
||||
# Only exists if Bazel was run |
||||
/bazel-out |
||||
|
||||
# dependencies |
||||
/node_modules |
||||
|
||||
# profiling files |
||||
chrome-profiler-events*.json |
||||
speed-measure-plugin*.json |
||||
|
||||
# IDEs and editors |
||||
/.idea |
||||
.project |
||||
.classpath |
||||
.c9/ |
||||
*.launch |
||||
.settings/ |
||||
*.sublime-workspace |
||||
|
||||
# IDE - VSCode |
||||
.vscode/* |
||||
!.vscode/settings.json |
||||
!.vscode/tasks.json |
||||
!.vscode/launch.json |
||||
!.vscode/extensions.json |
||||
.history/* |
||||
|
||||
# misc |
||||
/.sass-cache |
||||
/connect.lock |
||||
/coverage |
||||
/libpeerconnection.log |
||||
npm-debug.log |
||||
yarn-error.log |
||||
testem.log |
||||
/typings |
||||
|
||||
# System Files |
||||
.DS_Store |
||||
Thumbs.db |
||||
|
||||
# custom |
||||
|
||||
config.json |
||||
cache.json |
||||
|
@ -1,4 +1,4 @@
|
||||
# sp-status frontend |
||||
# GrafanaStatuspage |
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.4. |
||||
|
@ -0,0 +1,18 @@
|
||||
{ |
||||
"authToken": "test", |
||||
"title": "sp-status", |
||||
"description": "Services hosted by sp-codes", |
||||
"groups": [ |
||||
{ |
||||
"id": "test", |
||||
"name": "Test", |
||||
"services": [ |
||||
{ |
||||
"id": "test", |
||||
"name": "Test", |
||||
"url": "http://test.de" |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
@ -1,46 +0,0 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files. |
||||
|
||||
# compiled output |
||||
/dist |
||||
/tmp |
||||
/out-tsc |
||||
# Only exists if Bazel was run |
||||
/bazel-out |
||||
|
||||
# dependencies |
||||
/node_modules |
||||
|
||||
# profiling files |
||||
chrome-profiler-events*.json |
||||
speed-measure-plugin*.json |
||||
|
||||
# IDEs and editors |
||||
/.idea |
||||
.project |
||||
.classpath |
||||
.c9/ |
||||
*.launch |
||||
.settings/ |
||||
*.sublime-workspace |
||||
|
||||
# IDE - VSCode |
||||
.vscode/* |
||||
!.vscode/settings.json |
||||
!.vscode/tasks.json |
||||
!.vscode/launch.json |
||||
!.vscode/extensions.json |
||||
.history/* |
||||
|
||||
# misc |
||||
/.sass-cache |
||||
/connect.lock |
||||
/coverage |
||||
/libpeerconnection.log |
||||
npm-debug.log |
||||
yarn-error.log |
||||
testem.log |
||||
/typings |
||||
|
||||
# System Files |
||||
.DS_Store |
||||
Thumbs.db |
@ -1,53 +0,0 @@
|
||||
import {Injectable} from '@angular/core'; |
||||
import {Observable, of} from "rxjs"; |
||||
import {ApiResponse} from "../_data/data"; |
||||
|
||||
@Injectable({ |
||||
providedIn: 'root' |
||||
}) |
||||
export class ApiService { |
||||
constructor() { |
||||
} |
||||
|
||||
public getServiceStates(): Observable<ApiResponse> { |
||||
return of({ |
||||
state: "maintenance", |
||||
groups: [{ |
||||
id: 'default', |
||||
name: 'Some Group', |
||||
state: "outage", |
||||
services: [{ |
||||
id: 'nextcloud', |
||||
name: 'Nextcloud', |
||||
url: "https://sp-codes.de", |
||||
state: "operational" |
||||
}, { |
||||
id: 'synapse', |
||||
name: 'Synapse', |
||||
url: "https://sp-codes.de", |
||||
state: "outage" |
||||
}, { |
||||
id: 'searx', |
||||
name: 'Searx', |
||||
url: "https://sp-codes.de", |
||||
state: "maintenance" |
||||
}] |
||||
}, { |
||||
id: 'test', |
||||
name: 'Test', |
||||
state: "operational", |
||||
services: [{ |
||||
id: 'nextcloud', |
||||
name: 'Nextcloud', |
||||
url: "https://sp-codes.de", |
||||
state: "operational" |
||||
}, { |
||||
id: 'synapse', |
||||
name: 'Synapse', |
||||
url: "https://sp-codes.de", |
||||
state: "operational" |
||||
}] |
||||
}] |
||||
}); |
||||
} |
||||
} |
@ -1,9 +0,0 @@
|
||||
import { Component } from '@angular/core'; |
||||
|
||||
@Component({ |
||||
selector: 'app-root', |
||||
templateUrl: './app.component.html', |
||||
styleUrls: ['./app.component.scss'] |
||||
}) |
||||
export class AppComponent { |
||||
} |
@ -1,26 +0,0 @@
|
||||
import { BrowserModule } from '@angular/platform-browser'; |
||||
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"; |
||||
|
||||
@NgModule({ |
||||
declarations: [ |
||||
AppComponent, |
||||
StatusComponent |
||||
], |
||||
imports: [ |
||||
BrowserModule, |
||||
AppRoutingModule, |
||||
BrowserAnimationsModule, |
||||
MatExpansionModule, |
||||
MatListModule |
||||
], |
||||
providers: [], |
||||
bootstrap: [AppComponent] |
||||
}) |
||||
export class AppModule { } |
@ -1,3 +0,0 @@
|
||||
export const environment = { |
||||
production: true |
||||
}; |
@ -1,15 +0,0 @@
|
||||
<!doctype html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>sp-status</title> |
||||
<base href="/"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||
<link id="favicon" rel="icon" type="image/x-icon" href="favicon-operational.ico"> |
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet"> |
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> |
||||
</head> |
||||
<body class="mat-typography"> |
||||
<app-root></app-root> |
||||
</body> |
||||
</html> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
|
||||
import 'zone.js/dist/zone-node'; |
||||
|
||||
import {ngExpressEngine} from '@nguniversal/express-engine'; |
||||
import * as express from 'express'; |
||||
import {join} from 'path'; |
||||
|
||||
import {AppServerModule} from './src/main.server'; |
||||
import {APP_BASE_HREF} from '@angular/common'; |
||||
import {existsSync} from 'fs'; |
||||
import {api} from './src/main.status'; |
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app() { |
||||
const server = express(); |
||||
const distFolder = join(process.cwd(), 'dist/grafana-statuspage/browser'); |
||||
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.set('view engine', 'html'); |
||||
server.set('views', distFolder); |
||||
|
||||
server.use('/api', api); |
||||
|
||||
// Serve static files from /browser
|
||||
server.get('*.*', express.static(distFolder, { |
||||
maxAge: '1y' |
||||
})); |
||||
|
||||
// All regular routes use the Universal engine
|
||||
server.get('*', (req, res) => { |
||||
res.render(indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: req.baseUrl}]}); |
||||
}); |
||||
|
||||
return server; |
||||
} |
||||
|
||||
function run() { |
||||
const port = process.env.PORT || 4000; |
||||
|
||||
// Start up the Node server
|
||||
const server = app(); |
||||
server.listen(port, () => { |
||||
console.log(`Node Express server listening on http://localhost:${port}`); |
||||
}); |
||||
} |
||||
|
||||
// Webpack will replace 'require' with '__webpack_require__'
|
||||
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||
declare const __non_webpack_require__: NodeRequire; |
||||
const mainModule = __non_webpack_require__.main; |
||||
const moduleFilename = mainModule && mainModule.filename || ''; |
||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { |
||||
run(); |
||||
} |
||||
|
||||
export * from './src/main.server'; |
@ -0,0 +1,25 @@
|
||||
import {Inject, Injectable, PLATFORM_ID} from '@angular/core'; |
||||
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' |
||||
}) |
||||
export class ApiService { |
||||
private readonly api; |
||||
|
||||
constructor(private http: HttpClient, @Inject(PLATFORM_ID) platformId: Object) { |
||||
this.api = isPlatformBrowser(platformId) ? '/api' : environment.serverUrl + '/api'; |
||||
} |
||||
|
||||
public getServiceStates(): Observable<CurrentStatus> { |
||||
return this.http.get<CurrentStatus>(this.api+ '/status'); |
||||
} |
||||
|
||||
public getMetaInfo(): Observable<MetaInfo> { |
||||
return this.http.get<MetaInfo>(this.api+ '/info'); |
||||
} |
||||
} |
@ -1,7 +1,7 @@
|
||||
<div class="box"> |
||||
<header class="container pt-4"> |
||||
<h1>sp-status</h1> |
||||
<h3>Services hosted by sp-codes</h3> |
||||
<h1 *ngIf="title && title.length">{{title}}</h1> |
||||
<h3 *ngIf="description && description.length">{{description}}</h3> |
||||
</header> |
||||
|
||||
<main class="container"> |
@ -0,0 +1,26 @@
|
||||
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', |
||||
templateUrl: './app.component.html', |
||||
styleUrls: ['./app.component.scss'] |
||||
}) |
||||
export class AppComponent implements OnInit { |
||||
title: string; |
||||
description: string; |
||||
|
||||
constructor(private api: ApiService, private htmlTitle: Title) { |
||||
} |
||||
|
||||
ngOnInit(): void { |
||||
this.api.getMetaInfo().subscribe(info => { |
||||
this.title = info.title; |
||||
this.description = info.description; |
||||
this.htmlTitle.setTitle(this.title); |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
import {BrowserModule} from '@angular/platform-browser'; |
||||
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 {HttpClientModule} from "@angular/common/http"; |
||||
|
||||
@NgModule({ |
||||
declarations: [ |
||||
AppComponent, |
||||
StatusComponent |
||||
], |
||||
imports: [ |
||||
BrowserModule.withServerTransition({appId: 'serverApp'}), |
||||
AppRoutingModule, |
||||
BrowserAnimationsModule, |
||||
HttpClientModule, |
||||
MatExpansionModule, |
||||
MatListModule |
||||
], |
||||
providers: [], |
||||
bootstrap: [AppComponent] |
||||
}) |
||||
export class AppModule { |
||||
} |
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core'; |
||||
import { ServerModule } from '@angular/platform-server'; |
||||
|
||||
import { AppModule } from './app.module'; |
||||
import { AppComponent } from './app.component'; |
||||
|
||||
@NgModule({ |
||||
imports: [ |
||||
AppModule, |
||||
ServerModule, |
||||
], |
||||
bootstrap: [AppComponent], |
||||
}) |
||||
export class AppServerModule {} |
@ -0,0 +1,4 @@
|
||||
export const environment = { |
||||
production: true, |
||||
serverUrl: 'http://localhost:4000' |
||||
}; |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,13 @@
|
||||
<!doctype html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>grafana-statuspage</title> |
||||
<base href="/"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||
<link id="favicon" rel="icon" type="image/x-icon" href=""> |
||||
</head> |
||||
<body class="mat-typography"> |
||||
<app-root></app-root> |
||||
</body> |
||||
</html> |
@ -0,0 +1,10 @@
|
||||
import { enableProdMode } from '@angular/core'; |
||||
|
||||
import { environment } from './environments/environment'; |
||||
|
||||
if (environment.production) { |
||||
enableProdMode(); |
||||
} |
||||
|
||||
export { AppServerModule } from './app/app.server.module'; |
||||
export { renderModule, renderModuleFactory } from '@angular/platform-server'; |
@ -0,0 +1,117 @@
|
||||
import {json, Router} from 'express'; |
||||
import {CurrentStatus, State} from "./app/_data/data"; |
||||
import {existsSync, readFileSync, writeFileSync} from "fs"; |
||||
|
||||
interface Cache { |
||||
[id: string]: State |
||||
} |
||||
|
||||
interface Config { |
||||
authToken: string; |
||||
title: string; |
||||
description: string; |
||||
groups: { |
||||
id: string; |
||||
name: string; |
||||
services: { |
||||
id: string; |
||||
name: string; |
||||
url: string; |
||||
}[]; |
||||
}[]; |
||||
} |
||||
|
||||
interface GrafanaWebhookBody { |
||||
dashboardId: number; |
||||
evalMatches: { |
||||
value: number, |
||||
metric: string, |
||||
tags: any |
||||
}[]; |
||||
imageUrl: string, |
||||
message: string, |
||||
orgId: number, |
||||
panelId: number, |
||||
ruleId: number, |
||||
ruleName: string, |
||||
ruleUrl: string, |
||||
state: "ok" | "paused" | "alerting" | "pending" | "no_data"; |
||||
tags: { [key: string]: string }, |
||||
title: string |
||||
} |
||||
|
||||
const api = Router(); |
||||
api.use(json()); |
||||
|
||||
const config = JSON.parse(readFileSync('config.json', {encoding: 'UTF-8'})) as Config; |
||||
const serviceStates = existsSync('cache.json') ? JSON.parse(readFileSync('cache.json', {encoding: 'UTF-8'})) : {} as Cache; |
||||
|
||||
let cache: CurrentStatus; |
||||
updateCache(); |
||||
|
||||
api.post('/update/health', (req, res) => { |
||||
const token = req.query.token; |
||||
if (token !== config.authToken) { |
||||
return res.status(401).send('invalid token'); |
||||
} |
||||
const serviceId = req.query.service as string; |
||||
const message = req.body as GrafanaWebhookBody; |
||||
|
||||
switch (message.state) { |
||||
case "no_data": |
||||
case "alerting": |
||||
serviceStates[serviceId] = "outage"; |
||||
break; |
||||
case "paused": |
||||
serviceStates[serviceId] = "maintenance"; |
||||
break; |
||||
default: |
||||
serviceStates[serviceId] = "operational" |
||||
} |
||||
|
||||
updateCache(); |
||||
|
||||
writeFileSync('cache.json', JSON.stringify(serviceStates), {encoding: 'UTF-8'}); |
||||
|
||||
return res.send('OK'); |
||||
}); |
||||
|
||||
api.get('/status', (req, res) => { |
||||
return res.json(cache); |
||||
}); |
||||
|
||||
api.get('/info', (req, res) => { |
||||
return res.json({ |
||||
title: config.title, |
||||
description: config.description |
||||
}); |
||||
}); |
||||
|
||||
function updateCache(): void { |
||||
const groups = config.groups.map(group => { |
||||
const services = group.services.map(service => { |
||||
return { |
||||
id: service.id, |
||||
name: service.name, |
||||
url: service.url, |
||||
state: serviceStates[service.id] || "operational" |
||||
} |
||||
}); |
||||
return { |
||||
id: group.id, |
||||
name: group.name, |
||||
state: calculateOverallState(services.map(s => s.state)), |
||||
services: services |
||||
} |
||||
}); |
||||
cache = { |
||||
state: calculateOverallState(groups.map(g => g.state)), |
||||
groups: groups |
||||
}; |
||||
} |
||||
|
||||
function calculateOverallState(states: State[]): State { |
||||
return states.includes("outage") ? "outage" : states.includes("maintenance") ? "maintenance" : "operational" |
||||
} |
||||
|
||||
export {api}; |
@ -0,0 +1,17 @@
|
||||
{ |
||||
"extends": "./tsconfig.app.json", |
||||
"compilerOptions": { |
||||
"outDir": "./out-tsc/app-server", |
||||
"module": "commonjs", |
||||
"types": [ |
||||
"node" |
||||
] |
||||
}, |
||||
"files": [ |
||||
"src/main.server.ts", |
||||
"server.ts" |
||||
], |
||||
"angularCompilerOptions": { |
||||
"entryModule": "./src/app/app.server.module#AppServerModule" |
||||
} |
||||
} |
Loading…
Reference in new issue