Compare commits
10
Commits
d4b463b463
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f84a67456 | ||
|
|
e63d0dfee8 | ||
|
|
8e6d126324 | ||
|
|
7f59a46881 | ||
|
|
d00f26a672 | ||
|
|
77cc53f450 | ||
|
|
82c1022a66 | ||
|
|
caadde94db | ||
|
|
32ccdd2275 | ||
|
|
8ec3880ee0 |
@@ -40,8 +40,11 @@
|
||||
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [📚 Table of Contents](#-table-of-contents)
|
||||
- [About The Project](#about-the-project)
|
||||
- [Built With](#built-with)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Uninstalling](#uninstalling)
|
||||
|
||||
---
|
||||
|
||||
@@ -59,7 +62,18 @@
|
||||
|
||||
## Getting Started
|
||||
|
||||
Not yet :/
|
||||
```bash
|
||||
wget -qO- https://raw.githubusercontent.com/orus-dev/sentryx/refs/heads/master/install.sh | bash
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
```bash
|
||||
sh $HOME/sentryx-server/uninstall.sh
|
||||
```
|
||||
If that fails
|
||||
```bash
|
||||
wget -qO- https://raw.githubusercontent.com/orus-dev/sentryx/refs/heads/master/uninstall.sh | bash
|
||||
```
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/orus-dev/sentryx.svg?style=flat-square&color=blue&label=contributors
|
||||
[contributors-url]: https://github.com/orus-dev/sentryx/graphs/contributors
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"install": "sh test"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Check for required commands
|
||||
for cmd in node npm git sudo; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "Error: Required command '$cmd' is not installed or not in PATH. For more info: https://github.com/orus-dev/sentryx/wiki/Common-errors" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
REPO_URL="https://github.com/orus-dev/sentryx.git"
|
||||
INSTALL_DIR="$HOME/sentryx"
|
||||
SERVICE_NAME="sentryx"
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
NODE_BIN="$(which node)"
|
||||
RUN_USER="$(whoami)"
|
||||
|
||||
# Backup if exists
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Sentryx already exists, backing up..."
|
||||
rm -rf "$INSTALL_DIR-backup"
|
||||
mv "$INSTALL_DIR" "$INSTALL_DIR-backup"
|
||||
fi
|
||||
|
||||
# Clone as current user
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
|
||||
# Build as current user
|
||||
cd "$INSTALL_DIR"
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Create systemd service
|
||||
echo "Creating systemd service..."
|
||||
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=SentryX Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$RUN_USER
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart="$NODE_BIN" "$INSTALL_DIR/node_modules/.bin/next" start
|
||||
Restart=on-failure
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now "$SERVICE_NAME.service"
|
||||
|
||||
echo "Done! Check service with: sudo systemctl status $SERVICE_NAME"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sentryx",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
+1
-1
@@ -38,6 +38,6 @@ export default class Data<T> {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
writeFileSync(this.file_path, JSON.stringify(this.data));
|
||||
writeFileSync(this.file_path, JSON.stringify(this.data, undefined, 4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function average<T>(values: Array<T>, p: (v: T) => number) {
|
||||
: 0;
|
||||
}
|
||||
|
||||
const usageData = Array.from({length: 60}).map((_, i) => ({ hour: i, cpu: 0, ram: 0 }));
|
||||
const usageData = new Array(60).fill({cpu: 0, ram: 0});
|
||||
var i = 0;
|
||||
|
||||
setInterval(() => {
|
||||
@@ -28,7 +28,7 @@ setInterval(() => {
|
||||
};
|
||||
i++;
|
||||
}
|
||||
}, 60000);
|
||||
}, 1000);
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
@@ -47,7 +47,7 @@ export async function GET(request: Request) {
|
||||
const maxRam = Math.max(...slice.map((entry) => entry.ram));
|
||||
|
||||
peakData.push({
|
||||
hour: slice[0].hour,
|
||||
hour: i,
|
||||
cpu: maxCpu,
|
||||
ram: maxRam
|
||||
});
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@
|
||||
--popover-foreground: oklch(0.9108 0.0427 269.5516);
|
||||
--primary: oklch(0.7182 0.1105 239.784);
|
||||
--primary-foreground: oklch(0.3233 0.1025 253.8852);
|
||||
--secondary: oklch(0.5544 0.0407 257.4166);
|
||||
--secondary: oklch(0.38 0.06 258);
|
||||
--secondary-foreground: oklch(0.9288 0.0126 255.5078);
|
||||
--muted: oklch(0.3923 0.0613 254.3914);
|
||||
--muted-foreground: oklch(0.7584 0.0419 263.598);
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function NewServerDialog({
|
||||
disabled={name == "" || ip == ""}
|
||||
onClick={() => {
|
||||
session
|
||||
?.addServer({ name, ip, coordinates, location })
|
||||
?.addServer({ name, ip, coordinates, location, })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Server added successfully, refresh to view it"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import Session from "@/lib/session";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function AddAppDialog({
|
||||
session,
|
||||
}: {
|
||||
session: Session | null;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [ip, setIp] = useState("");
|
||||
const [location, setLocation] = useState<string | undefined>(undefined);
|
||||
const [coordinates, setCoordinates] = useState<[number, number] | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
className={cn(buttonVariants({ variant: "default" }))}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Plus className="m-0 p-0" /> New
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Application</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Label className="mt-2">
|
||||
Application Name<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Sentryx chat, My Project, etc"
|
||||
required
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<Label className="mt-2">
|
||||
Application service<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="my-app.service"
|
||||
required
|
||||
onInput={(e) => setIp(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={name == "" || ip == ""}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Add App
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import Session from "@/lib/session";
|
||||
import Service, { AppService } from "@/types/service";
|
||||
import { AppWindowMac } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export function UsageBar({
|
||||
value,
|
||||
Icon,
|
||||
text,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
Icon: React.ElementType;
|
||||
text: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="justify-between flex">
|
||||
<Label className="text-md flex justify-center">
|
||||
<Icon size={16} color={color} /> {text}
|
||||
</Label>
|
||||
<Label className="text-md">{value}%</Label>
|
||||
</div>
|
||||
<Progress value={value} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
const size = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
||||
|
||||
return `${size} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export function ByteUnit({
|
||||
value,
|
||||
Icon,
|
||||
text,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
Icon: React.ElementType;
|
||||
text: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="justify-between flex">
|
||||
<Label className="text-md flex justify-center">
|
||||
<Icon size={16} color={color} /> {text}
|
||||
</Label>
|
||||
<Label className="text-md">{formatBytes(value)}</Label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AppComponent({
|
||||
service,
|
||||
index,
|
||||
app
|
||||
}: {
|
||||
service: Service;
|
||||
app: AppService;
|
||||
index: number;
|
||||
}) {
|
||||
const [deleteAlert, setDeleteAlert] = useState(false);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer !select-none border-secondary border-2"
|
||||
onClick={() => {
|
||||
redirect(`/servers/${index}`);
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 overflow-clip">
|
||||
<div className="bg-muted text-muted-foreground p-1.5 rounded-md">
|
||||
<AppWindowMac className="w-4.5 h-4.5 text-current my-auto" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{app.name}</CardTitle>
|
||||
|
||||
{(service.description && <CardDescription className="flex justify-between">
|
||||
<div><p className={service.active_state}>{service.active_state[0].toUpperCase() + service.active_state.slice(1)}</p> {service.description}</div>
|
||||
{(service as any).cpu}
|
||||
</CardDescription>)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="w-full flex flex-col gap-3.5">
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import useSession from "@/hooks/useSession";
|
||||
import Server from "@/types/server";
|
||||
import { useEffect, useState } from "react";
|
||||
import ServiceComponent from "./service";
|
||||
import ServerComponent from "@/app/dashboard/server";
|
||||
import Service, { AppService } from "@/types/service";
|
||||
import AppComponent from "./AppComponent";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import AddAppDialog from "./AddAppDialog";
|
||||
import InstallAppDialog from "./IntallAppDialog";
|
||||
|
||||
interface Server2 extends Server {
|
||||
services: Service[],
|
||||
apps: AppService[],
|
||||
}
|
||||
|
||||
export default function ClientPage({ serverId }: { serverId: string }) {
|
||||
const index = parseInt(serverId) || 0;
|
||||
const session = useSession();
|
||||
const [server, setServer] = useState<Server2 | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
session?.getServerIndex(index).then((s) => {
|
||||
setServer(s.data);
|
||||
setInterval(() => {
|
||||
session?.getServerIndex(index).then((s) => {
|
||||
setServer(s.data);
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
}, [session]);
|
||||
|
||||
return server && (
|
||||
<div className="gap-4 flex flex-col">
|
||||
<div>
|
||||
<ServerComponent server={server} session={session} index={index} nonInteractive />
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader className="flex">
|
||||
<div>
|
||||
<CardTitle>Sentryx Applications</CardTitle>
|
||||
<CardDescription>Your Sentryx apps on this server</CardDescription>
|
||||
</div>
|
||||
<div className="ml-auto w-max flex gap-3">
|
||||
<InstallAppDialog session={session} />
|
||||
<AddAppDialog session={session} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||
{server.apps.map((a, i) => {
|
||||
const srv = server.services.find(s => s.name === a.service);
|
||||
return srv && <AppComponent key={i} service={srv} index={i} app={a} />;
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||
{server.services.map((a, i) => (
|
||||
<ServiceComponent key={i} service={a} index={i} session={session} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { HardDriveDownload, Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import Session from "@/lib/session";
|
||||
import { toast } from "sonner";
|
||||
import { FileUpload } from "@/components/FileUpload";
|
||||
|
||||
export default function InstallAppDialog({
|
||||
session,
|
||||
}: {
|
||||
session: Session | null;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [script, setScript] = useState<{ install?: string } | undefined>({});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
className={cn(buttonVariants({ variant: "secondary" }))}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<HardDriveDownload className="m-0 p-0" /> Install
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Install Application</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Label className="mt-2">
|
||||
Application Name<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Sentryx chat, My Project, etc"
|
||||
required
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<Label className="mt-2">
|
||||
Application script<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<FileUpload setSelected={(contents) => {
|
||||
try {
|
||||
setScript(JSON.parse(contents))
|
||||
console.log(contents)
|
||||
} catch {
|
||||
setScript(undefined)
|
||||
}
|
||||
}} />
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={name == "" || !script || !(script.install)}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,5 @@
|
||||
"use client";
|
||||
import useSession from "@/hooks/useSession";
|
||||
import Server from "@/types/server";
|
||||
import { useEffect, useState } from "react";
|
||||
import ServiceComponent from "./service";
|
||||
import ServerComponent from "@/app/dashboard/server";
|
||||
import Service, { AppService } from "@/types/service";
|
||||
import ClientPage from './ClientPage';
|
||||
|
||||
interface Server2 extends Server {
|
||||
services: Service[],
|
||||
apps: AppService[],
|
||||
}
|
||||
|
||||
export default function ServerPage(params: { server: string }) {
|
||||
const index = parseInt(params.server) || 0;
|
||||
const session = useSession();
|
||||
const [server, setServer] = useState<Server2 | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
session?.getServerIndex(index).then((s) => {
|
||||
setServer(s.data);
|
||||
setInterval(() => {
|
||||
session?.getServerIndex(index).then((s) => {
|
||||
setServer(s.data);
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
}, [session]);
|
||||
|
||||
return server && (
|
||||
<div className="gap-4 flex flex-col">
|
||||
<div>
|
||||
<ServerComponent server={server} session={session} index={index} nonInteractive />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||
{server.services.map((a, i) => (
|
||||
<ServiceComponent key={i} service={a} index={i} session={session} / >
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export default async function ServerPage({ params }: { params: Promise<{ server: string }> }) {
|
||||
return <ClientPage serverId={(await params).server} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useState, useRef } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils" // optional utility for combining classes
|
||||
|
||||
export function FileUpload({setSelected} : {setSelected: (contents: string) => void}) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [fileName, setFileName] = useState<string | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isUnsupported, setIsUnsupported] = useState(false)
|
||||
|
||||
const handleFiles = (files: FileList | null) => {
|
||||
const file = files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsUnsupported(!file.name.endsWith('.json'));
|
||||
if (isUnsupported) return
|
||||
|
||||
setFileName(file.name)
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setSelected(e.target?.result as string)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
handleFiles(e.dataTransfer.files)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div
|
||||
onClick={() => inputRef.current?.click()}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
className={cn(
|
||||
"flex h-32 cursor-pointer items-center justify-center rounded-md border-2 border-dashed transition-colors",
|
||||
isDragging ? "border-primary bg-muted" : "border-muted",
|
||||
isUnsupported && "border-destructive"
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{isUnsupported ? "Unsupported file type" : (fileName || "Click or drag file to upload")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ export interface ServerAPI {
|
||||
ip: string;
|
||||
location?: string;
|
||||
coordinates?: [number, number];
|
||||
apps: AppService[];
|
||||
apps?: AppService[];
|
||||
}
|
||||
|
||||
export default interface Server extends ServerAPI {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export interface AppService { name?: string, description?: string };
|
||||
export interface AppService { name?: string, description?: string, service: string };
|
||||
export default interface Service { name?: string, description?: string, active_state: string };
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SERVICE_NAME="sentryx"
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
INSTALL_DIR="$HOME/sentryx"
|
||||
|
||||
echo "Stopping and disabling systemd service..."
|
||||
sudo systemctl stop "$SERVICE_NAME.service" || true
|
||||
sudo systemctl disable "$SERVICE_NAME.service" || true
|
||||
|
||||
echo "Removing systemd service file..."
|
||||
sudo rm -f "$SERVICE_FILE"
|
||||
|
||||
echo "Reloading systemd daemon..."
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
echo "Removing installation directory..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
rm -rf "${INSTALL_DIR}-backup"
|
||||
|
||||
echo "Uninstall complete."
|
||||
Reference in New Issue
Block a user