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 =
+
+ }
+ else if (fullData) {
+ content =
+
+ }
+ 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
-
+
+ {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