forked from MapComplete/MapComplete
		
	Add simple status page
This commit is contained in:
		
							parent
							
								
									bdea29eea1
								
							
						
					
					
						commit
						811bcecea4
					
				
					 6 changed files with 354 additions and 3 deletions
				
			
		|  | @ -21,9 +21,9 @@ | ||||||
|       "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", |       "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", | ||||||
|       "url": "https://www.openstreetmap.org" |       "url": "https://www.openstreetmap.org" | ||||||
|     }, |     }, | ||||||
|     "mvt_layer_server": "https://proxy.mapcomplete.org/public.{type}_{layer}/{z}/{x}/{y}.pbf", |     "mvt_layer_server": "https://cache.mapcomplete.org/public.{type}_{layer}/{z}/{x}/{y}.pbf", | ||||||
|     "#summary_server": "Should be the endpoint; appending status.json should work", |     "#summary_server": "Should be the endpoint; appending status.json should work", | ||||||
|     "summary_server": "https://proxy0.mapcomplete.org/", |     "summary_server": "https://cache.mapcomplete.org/", | ||||||
|     "geoip_server": "https://ipinfo.mapcomplete.org/", |     "geoip_server": "https://ipinfo.mapcomplete.org/", | ||||||
|     "error_server": "https://report.mapcomplete.org/report", |     "error_server": "https://report.mapcomplete.org/report", | ||||||
|     "disabled:oauth_credentials": { |     "disabled:oauth_credentials": { | ||||||
|  | @ -93,7 +93,7 @@ | ||||||
|     "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", |     "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", | ||||||
| 
 | 
 | ||||||
|     "clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm", |     "clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm", | ||||||
|     "clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|privacy\\|test\\|studio\\|theme\\|style_test\\|statistics\\|leaderboard\\).html\" | xargs -r rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs -r rm)", |     "clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|privacy\\|test\\|studio\\|theme\\|style_test\\|statistics\\|status\\|leaderboard\\).html\" | xargs -r rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs -r rm)", | ||||||
| 
 | 
 | ||||||
|     "generate:dependency-graph": "node_modules/.bin/depcruise --exclude \"^node_modules\" --output-type dot Logic/State/MapState.ts > dependencies.dot && dot dependencies.dot -T svg -o dependencies.svg && rm dependencies.dot", |     "generate:dependency-graph": "node_modules/.bin/depcruise --exclude \"^node_modules\" --output-type dot Logic/State/MapState.ts > dependencies.dot && dot dependencies.dot -T svg -o dependencies.svg && rm dependencies.dot", | ||||||
|     "scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/", |     "scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/", | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								src/UI/Status/MCService.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/UI/Status/MCService.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import { Store } from "../../Logic/UIEventSource" | ||||||
|  | 
 | ||||||
|  | export interface MCService { | ||||||
|  |     name: string | ||||||
|  |     status: Store<"online" | "degraded" | "offline">, | ||||||
|  |     message?: Store<undefined | string> | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								src/UI/Status/ServiceIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/UI/Status/ServiceIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | import StatusIcon from "./StatusIcon.svelte" | ||||||
|  | import type { MCService } from "./MCService.js" | ||||||
|  | import AccordionSingle from "../Flowbite/AccordionSingle.svelte" | ||||||
|  | 
 | ||||||
|  | export let service: MCService | ||||||
|  | let status = service.status | ||||||
|  | let msg = service.message | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <AccordionSingle> | ||||||
|  | 
 | ||||||
|  | <h3 slot="header" class="flex items-center m-0"> <StatusIcon status={$status}/> {service.name}</h3> | ||||||
|  | <div class="mx-4"> | ||||||
|  |   {#if $msg} | ||||||
|  |   {$msg} | ||||||
|  |     {:else} | ||||||
|  |     No extra information available | ||||||
|  |     {/if} | ||||||
|  | </div> | ||||||
|  | </AccordionSingle> | ||||||
							
								
								
									
										285
									
								
								src/UI/Status/StatusGUI.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								src/UI/Status/StatusGUI.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,285 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 
 | ||||||
|  |   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  |   import StatusIcon from "./StatusIcon.svelte" | ||||||
|  |   import type { MCService } from "./MCService" | ||||||
|  |   import ServiceIndicator from "./ServiceIndicator.svelte" | ||||||
|  |   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
|  |   import Constants from "../../Models/Constants" | ||||||
|  |   import { Utils } from "../../Utils" | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   let services: MCService[] = [] | ||||||
|  | 
 | ||||||
|  |   let states = [undefined, "online", "degraded", "offline"] | ||||||
|  | 
 | ||||||
|  |   function simpleMessage(s: Store<{ success: any } | { error: any }>): Store<string> { | ||||||
|  |     return s.mapD(s => { | ||||||
|  |       if (s["success"]) { | ||||||
|  |         return JSON.stringify(s["success"]) | ||||||
|  |       } | ||||||
|  |       return s["error"] | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const connection = new OsmConnection() | ||||||
|  |     const osmApi = connection.apiIsOnline | ||||||
|  |     services.push({ | ||||||
|  |       name: connection.Backend(), | ||||||
|  |       status: osmApi.mapD(serviceState => { | ||||||
|  |         switch (serviceState) { | ||||||
|  |           case "offline": | ||||||
|  |             return "offline" | ||||||
|  |           case "online": | ||||||
|  |             return "online" | ||||||
|  |           case "readonly": | ||||||
|  |             return "degraded" | ||||||
|  |           case "unknown": | ||||||
|  |             return undefined | ||||||
|  |           case "unreachable": | ||||||
|  |             return "offline" | ||||||
|  |         } | ||||||
|  |       }), | ||||||
|  |       message: osmApi | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = "https://studio.mapcomplete.org" | ||||||
|  |     const status = UIEventSource.FromPromiseWithErr( | ||||||
|  |       Utils.downloadJson(s + "/overview") | ||||||
|  |     ) | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         const files: string[] = s["success"]["allFiles"] | ||||||
|  |         if (files.length < 10) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         if (files.length < 100) { | ||||||
|  |           return "degraded" | ||||||
|  |         } | ||||||
|  |         return "online" | ||||||
|  |       }), | ||||||
|  |       message: status.mapD(s => { | ||||||
|  |         if(s["error"]){ | ||||||
|  |           return s["error"] | ||||||
|  |         } | ||||||
|  |         const files: string[] = s["success"]["allFiles"] | ||||||
|  |         return "Contains "+(files.length ?? "no")+" files" | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   { | ||||||
|  |     services.push( | ||||||
|  |       { | ||||||
|  |         name: Constants.GeoIpServer, | ||||||
|  |         status: UIEventSource.FromPromiseWithErr( | ||||||
|  |           Utils.downloadJson(Constants.GeoIpServer + "/status") | ||||||
|  |         ).mapD(result => { | ||||||
|  |           if (result["success"].online) { | ||||||
|  |             return "online" | ||||||
|  |           } | ||||||
|  |           if (result["error"]) { | ||||||
|  |             return "offline" | ||||||
|  |           } else { | ||||||
|  |             return "degraded" | ||||||
|  |           } | ||||||
|  |         }), | ||||||
|  |         message: simpleMessage(UIEventSource.FromPromiseWithErr( | ||||||
|  |           Utils.downloadJson(Constants.GeoIpServer + "/ip") | ||||||
|  |         )) | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = Constants.ErrorReportServer | ||||||
|  |     const status = UIEventSource.FromPromiseWithErr( | ||||||
|  |       Utils.downloadJson(s.replace(/\/report$/, "/status")) | ||||||
|  |     ) | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         const data = s["success"] | ||||||
|  |         if (data["errors_today"] === 0) { | ||||||
|  |           return "online" | ||||||
|  |         } | ||||||
|  |         return "degraded" | ||||||
|  |       }), | ||||||
|  |       message: simpleMessage(status) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = Constants.linkedDataProxy.replace(/\/[^/]*$/, "") | ||||||
|  |     const status = UIEventSource.FromPromiseWithErr( | ||||||
|  |       Utils.downloadJson(s + "/status") | ||||||
|  |     ) | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         const data = s["success"] | ||||||
|  |         if (data.cached_entries < 10 || data.uptime < 60 * 60) { | ||||||
|  |           return "degraded" | ||||||
|  |         } | ||||||
|  |         return "online" | ||||||
|  |       }), | ||||||
|  |       message: simpleMessage(status) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = Constants.SummaryServer | ||||||
|  |     const status = UIEventSource.FromPromiseWithErr( | ||||||
|  |       Utils.downloadJson(s + "/summary/status.json") | ||||||
|  |     ) | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         console.log(s) | ||||||
|  | 
 | ||||||
|  |         const attributes = s["success"]["meta"] | ||||||
|  |         const lastUpdate = new Date(attributes["current_timestamp"]) | ||||||
|  |         console.log("Last update:", lastUpdate, attributes["current_timestamp"], attributes) | ||||||
|  |         const timediffSec = (new Date().getTime() - lastUpdate.getTime()) / 1000 | ||||||
|  |         const timediffDays = timediffSec / (60 * 60 * 26) | ||||||
|  | 
 | ||||||
|  |         if (timediffDays > 7) { | ||||||
|  |           return "degraded" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return "online" | ||||||
|  |       }), | ||||||
|  |       message: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return s["error"] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const attributes = s["success"]["meta"] | ||||||
|  |         const lastUpdate = new Date(attributes["current_timestamp"]) | ||||||
|  |         const timediffSec = (new Date().getTime() - lastUpdate.getTime()) / 1000 | ||||||
|  |         const timediffDays = timediffSec / (60 * 60 * 26) | ||||||
|  | 
 | ||||||
|  |         const json = JSON.stringify(s["success"], null, "  ") | ||||||
|  |         return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = Constants.countryCoderEndpoint | ||||||
|  |     const status = UIEventSource.FromPromiseWithErr( | ||||||
|  |       Utils.downloadJson(s + "/0.0.0.json") | ||||||
|  |     ) | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         const arr = s["success"] | ||||||
|  |         if (Array.isArray(arr)) { | ||||||
|  |           return "online" | ||||||
|  |         } | ||||||
|  |         return "degraded" | ||||||
|  |       }), | ||||||
|  |       message: status.map(s => JSON.stringify(s)) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     for (const defaultOverpassUrl of Constants.defaultOverpassUrls) { | ||||||
|  |       const statusUrl = defaultOverpassUrl.replace(/\/interpreter$/, "/status") | ||||||
|  |       const status = UIEventSource.FromPromiseWithErr( | ||||||
|  |         Utils.download(statusUrl) | ||||||
|  |       ) | ||||||
|  | 
 | ||||||
|  |       services.push({ | ||||||
|  |         name: "Overpass-server: " + defaultOverpassUrl, | ||||||
|  |         status: status.mapD(result => { | ||||||
|  |           if (result["error"]) { | ||||||
|  |             return "offline" | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           // "Connected as: 3587935836 | ||||||
|  |           // Current time: 2024-07-14T00:35:58Z | ||||||
|  |           // Announced endpoint: gall.openstreetmap.de | ||||||
|  |           // Rate limit: 6 | ||||||
|  |           // 6 slots available now. | ||||||
|  |           // Currently running queries (pid, space limit, time limit, start time):\n" | ||||||
|  |           const msgs = result["success"].split("\n") | ||||||
|  | 
 | ||||||
|  |           return "online" | ||||||
|  |         }), | ||||||
|  |         message: simpleMessage(status) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     services.push({ | ||||||
|  |       name: "Mangrove reviews", | ||||||
|  |       status: UIEventSource.FromPromiseWithErr( | ||||||
|  |         Utils.download("https://api.mangrove.reviews") | ||||||
|  |       ).mapD(r => { | ||||||
|  |         if (r["success"]) { | ||||||
|  |           return "online" | ||||||
|  |         } | ||||||
|  |         return "offline" | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   let all = new UIEventSource<"online" | "degraded" | "offline">("online") | ||||||
|  |   let someLoading = new UIEventSource(true) | ||||||
|  | 
 | ||||||
|  |   function setAll() { | ||||||
|  |     const data = Utils.NoNull(services.map(s => s.status.data)) | ||||||
|  |     someLoading.setData(data.length !== services.length) | ||||||
|  |     if (data.some(d => d === "offline")) { | ||||||
|  |       all.setData("offline") | ||||||
|  |     } else if (data.some(d => d === "degraded")) { | ||||||
|  |       all.setData("degraded") | ||||||
|  |     } else if (data.some(d => d === "online")) { | ||||||
|  |       all.setData("online") | ||||||
|  |     } else { | ||||||
|  |       all.setData(undefined) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   for (const service of services) { | ||||||
|  |     service.status.addCallbackD(() => { | ||||||
|  |       setAll() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <h1>MapComplete status indicators</h1> | ||||||
|  | 
 | ||||||
|  | {#if $someLoading} | ||||||
|  |   <Loading /> | ||||||
|  | {/if} | ||||||
|  | <StatusIcon status={$all} cls="w-16 h-16" /> | ||||||
|  | 
 | ||||||
|  | {#each services as service} | ||||||
|  |   <ServiceIndicator {service} /> | ||||||
|  | {/each} | ||||||
							
								
								
									
										26
									
								
								src/UI/Status/StatusIcon.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/UI/Status/StatusIcon.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
|  |   import CheckCircle from "@babeard/svelte-heroicons/mini/CheckCircle" | ||||||
|  |   import XCircle from "@rgossiaux/svelte-heroicons/solid/XCircle" | ||||||
|  |   import { twJoin } from "tailwind-merge" | ||||||
|  |   import { XIcon } from "@rgossiaux/svelte-heroicons/outline" | ||||||
|  |   import Exclamation from "@rgossiaux/svelte-heroicons/solid/Exclamation" | ||||||
|  |   import Check from "@babeard/svelte-heroicons/mini/Check" | ||||||
|  |   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|  | 
 | ||||||
|  |   export let status: "online" | "degraded" | "offline" | ||||||
|  |   export let cls: string = "w-6 h-6 mx-1" | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if status === "online"} | ||||||
|  |   <CheckCircle class={twJoin(cls,"rounded-full")} style="color: #22cc22" /> | ||||||
|  | {:else if status === "degraded"} | ||||||
|  |   <Exclamation class={twJoin(cls,"rounded-full")} style="color: #eecc22"  /> | ||||||
|  | {:else if status === "offline"} | ||||||
|  |   <XCircleIcon class={twJoin(cls,"rounded-full")} style="color: #bb2222"  /> | ||||||
|  | {:else if status === undefined} | ||||||
|  |   <Loading /> | ||||||
|  | {:else} | ||||||
|  |   ? {status} | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
							
								
								
									
										11
									
								
								src/UI/StatusGui.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/UI/StatusGui.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import StatusGUI from "./Status/StatusGUI.svelte" | ||||||
|  | 
 | ||||||
|  | export default class StatusGui { | ||||||
|  |     public setup() { | ||||||
|  |         new StatusGUI({ | ||||||
|  |             target: document.getElementById("main"), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | new StatusGui().setup() | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue