diff --git a/interfaces/LinkTypes.ts b/interfaces/LinkTypes.ts index 07932fb..9aabf4a 100644 --- a/interfaces/LinkTypes.ts +++ b/interfaces/LinkTypes.ts @@ -1,15 +1,42 @@ -export interface LinkList { - services: CustomLink[], +export interface EntryList { + services: Service[], games: CustomLink[] } export interface CustomLink { + name: string, + href: string, + desc: string, + ip: string, type: string, + location: string, + status: string, + docker_container_name: string +} + +export interface Service { name: string, href: string, desc: string, warn: string, - ip: string, - location: string, - status: string, - docker_container_name: string + type: ServiceType, + docker_container_name: string, + location: ServiceLocation, + status: ServiceStatus +} + +export enum ServiceStatus { + online = "Online", + offline = "Offline", + loading = "Loading", + error = "ERROR" +} + +export enum ServiceLocation { + brr7_4800u = "brr7-4800u", + other = "" +} + +export enum ServiceType { + docker = "docker", + app = "app" } \ No newline at end of file diff --git a/package.json b/package.json index 94bd905..1dd3d52 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev:debug": "NODE_OPTIONS='--inspect' next dev -p 4040", - "dev": "next dev", + "dev": "next dev -p 4040", "build": "next build", "start": "next start", "lint": "next lint" @@ -17,10 +17,10 @@ "swr": "^1.3.0" }, "devDependencies": { - "eslint": "^8.23.1", - "eslint-config-next": "12.2.0", "@types/dockerode": "^3.3.14", "@types/react": "^18.0.14", + "eslint": "^8.23.1", + "eslint-config-next": "12.2.0", "typescript": "^4.7.4" } } diff --git a/pages/api/containers.tsx b/pages/api/containers.tsx new file mode 100644 index 0000000..6f75e6e --- /dev/null +++ b/pages/api/containers.tsx @@ -0,0 +1,18 @@ +import Docker from 'dockerode' + +export default async function ContainersAPI(req: any, res: any) { + try { + const options = { + socketPath: '/var/run/docker.sock', + path: '/v1.41/containers/json' + }; + var docker = new Docker({ socketPath: options.socketPath }); + const list = await docker.listContainers({ all: true }) + + res.status(200).json(list); + } + catch (error) { + console.log(error); + res.status(500).json({ error: 'Error reading data' }); + } +} \ No newline at end of file diff --git a/pages/api/services.tsx b/pages/api/services.tsx new file mode 100644 index 0000000..a416716 --- /dev/null +++ b/pages/api/services.tsx @@ -0,0 +1,21 @@ +import fsPromises from 'fs/promises' +import path from 'path' +import { Service, ServiceStatus } from '../../interfaces/LinkTypes'; + +export default async function ServicesAPI(req: any, res: any) { + try { + const filePath = path.join(process.cwd(), '/public/pages.json') + const data = await fsPromises.readFile(filePath) + .then((file) => JSON.parse(file.toString())); + data.services.forEach((service: Service) => { + service.status = ServiceStatus.loading; + }); + + res.status(200).json(data.services); + } + catch (error) { + console.log(error); + res.status(500).json({ error: 'Error reading data' }); + } +} + diff --git a/pages/games.tsx b/pages/games.tsx index 17ce412..67577c9 100644 --- a/pages/games.tsx +++ b/pages/games.tsx @@ -3,9 +3,9 @@ import Link from 'next/link' import styles from '/styles/Home.module.css' import fsPromises from 'fs/promises' import path from 'path' -import type { CustomLink, LinkList } from '../interfaces/LinkTypes' +import type { CustomLink, EntryList } from '../interfaces/LinkTypes' -function Servers(props: LinkList) { +function Servers(props: EntryList) { const serverList = props.games return ( <> diff --git a/pages/services.tsx b/pages/services.tsx index 2ce661c..1426db7 100644 --- a/pages/services.tsx +++ b/pages/services.tsx @@ -1,20 +1,65 @@ import Head from 'next/head' import Link from 'next/link' import styles from '/styles/Home.module.css' -import fsPromises from 'fs/promises' -import path, { resolve } from 'path' -import type { CustomLink, LinkList } from '../interfaces/LinkTypes' +import { Service, ServiceStatus, ServiceType, ServiceLocation } from '../interfaces/LinkTypes'; import Dockerode from 'dockerode'; +import { ReactElement } from 'react' +import useSWR from 'swr'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()) + +//function Services(props: EntryList) { +function Services() { + const { initialData, fullData, loadingInitial, loadingFull, error } = useServices(); -function Services(props: LinkList) { - const serviceList = props.services + let content: ReactElement = <>; + + if (error) { content =
Error loading data
} + else if (loadingInitial) { + content =
Loading
+ } + else if (loadingFull) { + content = +
+ {initialData?.map((item: Service) => ( + + +

{item.name}

+
{item.status}
+

{item.desc}

+

{item.warn}

+
+ + ))} +
+ } + else if (fullData) { + content = +
+ {fullData.map((item: Service) => ( + + +

{item.name}

+
{item.status}
+

{item.desc}

+

{item.warn}

+
+ + ))} +
+ } + else { + content =
Error loading data
+ } + return ( <> Neshura Servers +

@@ -24,116 +69,130 @@ function Services(props: LinkList) {

Lists all available Services, most likely up-to-date

-
- {serviceList.map((item: CustomLink) => ( - - -

{item.name}

-
{item.status}
-

{item.desc}

-

{item.warn}

-
- - ))} -
+ + {content} ) } -// Gets a List of all services specified in /public/pages.json -export async function getServerSideProps() { - const filePath = path.join(process.cwd(), '/public/pages.json') - // TODO: look into asyncing this API call - const jsonData = await fsPromises.readFile(filePath) - const list = JSON.parse(jsonData.toString()) - for (let index = 0; index < list.services.length; index++) { - // TODO: look into asyncing this as well - await status(list.services[index]); +async function getStatus(entry: Service, containers: Dockerode.ContainerInfo[]) { + // Currently the only location supporting different fetching depending on type is brr7-4800u + // Others to follow but low prio as this is currently the only location used + + // Location BRR7-4800U + if (entry.location === ServiceLocation.brr7_4800u) { + // Type APP + if (entry.type === ServiceType.app) { + await fetch(entry.href) + .then((response) => { + if (response.ok) { + switch (response.status) { + case 200: + case 301: + case 302: + entry.status = ServiceStatus.online; + break; + default: + entry.status = ServiceStatus.offline; + } + } + else { + entry.status = ServiceStatus.offline; + } + }) + .catch((error) => { + console.error("Error pinging Website: ", error); + entry.status = ServiceStatus.error; + }) + } + // Type Docker + else if (entry.type === ServiceType.docker) { + for (let i = 0; i < containers.length; i++) { + const container = containers[i]; + // Docker API returns container names with / prepended + if (containers[i].Names.includes("/" + entry.docker_container_name)) { + // so far only "running" is properly implemented, mroe cases to follow as needed + switch (container.State) { + case "running": + entry.status = ServiceStatus.online; + break; + default: + console.log("Container Status " + container.State + " has no case implemented"); + entry.status = ServiceStatus.offline; + } + // cancel the for + break; + } + // If container name is not missing the container is set to offline + else if (entry.docker_container_name !== null) { + console.warn("Container for " + entry.name + " could not be found"); + entry.status = ServiceStatus.offline; + } + else { + console.error("Container Name not specified"); + entry.status = ServiceStatus.error; + } + } + } + // If no Type matches + else { + console.warn("Service Type for Service " + entry.name + " not specified or invalid"); + entry.status = ServiceStatus.error; + } } - return { props: list } + // Location Other + // TODO: implement docker type for other locations + else if (entry.location === ServiceLocation.other) { + // Currently uses the same handling as app type for the other location + await fetch(entry.href) + .then((response) => { + if (response.ok) { + switch (response.status) { + case 200: + case 301: + case 302: + entry.status = ServiceStatus.online; + break; + default: + entry.status = ServiceStatus.offline; + } + } + else { + entry.status = ServiceStatus.offline; + } + }) + .catch((error) => { + console.error("Error pinging Website: ", error); + entry.status = ServiceStatus.error; + }) + } + // If no Location matches + else { + console.warn("Service Location for Service " + entry.name + " not specified"); + entry.status = ServiceStatus.error; + } + return entry; } -// reversing this to loop over given entries for every found container would probably improve latency -async function status(entry: CustomLink) { - // for now only the BRR7-4800U can be used with Docker, needs changing once more Servers are used - // TODO: support multiple locations for docker - if (entry.location === "brr7-4800u") { - // app in this context means any non-docker page - if (entry.type === "app") { +const fetchFullDataArray = (containerData: Dockerode.ContainerInfo[], dataSet: Service[]) => { + const fetchStatus = (entry: Service) => getStatus(entry, containerData); + return Promise.all(dataSet.map(fetchStatus)); +} - let data = new Response(); - try { - await fetch(entry.href).then((response: Response) => data = response); - } - catch (e) { - console.log(e) - return (entry.status = "Offline"); - } +function useServices() { + const { data: containerData, error: containerError } = useSWR('/api/containers', fetcher); + const { data: initialData, error: initialError } = useSWR('/api/services', fetcher); + const loadingInitial = !initialData && !initialError + const { data: fullData, error: fullError } = useSWR((initialData && containerData) ? [containerData, initialData] : null, fetchFullDataArray) + const loadingFull = !fullData && !fullError - if (data.ok) { - if (data.status == 200 || data.status == 301 || data.status == 302) { - return (entry.status = "Online"); - } - else return (entry.status = "Offline"); - } - else return (entry.status = "Offline"); - } - else if (entry.type === "docker") { - var Docker = require('dockerode'); - - // TODO: read these paths from some config instead of hardcoding them - const options = { - socketPath: '/var/run/docker.sock', - path: '/v1.41/containers/json' - }; - var docker = new Docker({ socketPath: options.socketPath }); - - // default is set as Offline, prevents uncaught cases without set status - entry.status = "Offline"; - // TODO: async possible? - await docker.listContainers({all: true}).then( - ((containers: Dockerode.ContainerInfo[]) => { - // Loop over every found container and compare to the entry provided - containers.forEach((element: Dockerode.ContainerInfo) => { - element.Names.forEach((containerName: string) => { - if (containerName.startsWith("/")) { - containerName = containerName.substring(1); - } - if (containerName === entry.docker_container_name) { - entry.status = "Online"; - } - }); - if (entry.docker_container_name == null) { - console.log("MISSING DOCKER CONTAINER NAME FOR " + entry.name); - entry.status = "ERROR"; - } - }); - }) - ); - } - else { entry.status = "ERROR" } - return; - } - // for non-local locations pinging can be used to see if they're up - else if (entry.location != "") { - let data: Response; - try { - data = await fetch(entry.href); - } - catch (e) { - console.log(e) - return (entry.status = "Offline"); - } - - if (data.ok) { - if (data.status == 200 || data.status == 301 || data.status == 302) { - return (entry.status = "Online"); - } - else return (entry.status = "Offline"); - } - else return (entry.status = "Offline"); - } - else { return (entry.status = "ERROR") } + return { + initialData, + fullData, + loadingInitial, + loadingFull, + error: initialError || fullError || containerError, + }; } export default Services \ No newline at end of file