Merge branch 'main' into add-prometheus

This commit is contained in:
Andrew Erlikh 2025-03-21 15:42:34 +07:00 committed by GitHub
commit 70f2a9aeee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 2737 additions and 135 deletions

View file

@ -5,7 +5,6 @@ import (
// "net/http"
// _ "net/http/pprof"
_ "time/tzdata"
"github.com/aceberg/WatchYourLAN/internal/web"
)

24
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

17
frontend/Makefile Normal file
View file

@ -0,0 +1,17 @@
PKG_NAME=WatchYourLAN
USR_NAME=aceberg
build:
npm run build && \
rm ../internal/web/public/assets/* && \
cp -r dist/assets ../internal/web/public
replace:
cd ../internal/web/public/assets/ && \
cat index-*.js | sed 's/assets/fs\/public\/assets/g;s/http:\/\/0.0.0.0:8840//' > index.js && \
cat index-*.css | sed 's/assets/fs\/public\/assets/g' > index.css && \
cat HostPage-*.js | sed 's/index-.*\.js/index.js/g' > tmp && \
cp tmp HostPage-*.js && \
rm tmp index-*
all: build replace

28
frontend/README.md Normal file
View file

@ -0,0 +1,28 @@
## Usage
```bash
$ npm install # or pnpm install or yarn install
```
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
## Available Scripts
In the project directory, you can run:
### `npm run dev`
Runs the app in the development mode.<br>
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
### `npm run build`
Builds the app for production to the `dist` folder.<br>
It correctly bundles Solid in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
## Deployment
Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html)

13
frontend/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WatchYourLAN</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

1654
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

20
frontend/package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "watchyourlan",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5"
},
"devDependencies": {
"typescript": "~5.7.2",
"vite": "^6.2.0",
"vite-plugin-solid": "^2.11.2"
}
}

10
frontend/src/App.css Normal file
View file

@ -0,0 +1,10 @@
/* Sort button */
.my-btn {
height: 100%;
background-color: #00000000;
color: var(--bs-primary);
text-align: center;
}
.my-btn:hover {
background-color: #0000001a;
}

34
frontend/src/App.tsx Normal file
View file

@ -0,0 +1,34 @@
import { lazy, onMount } from 'solid-js';
import { Router, Route } from "@solidjs/router";
import './App.css';
import { runAtStart } from './functions/atstart';
import Body from './pages/Body';
import Header from './components/Header';
function App() {
onMount(() => {
runAtStart();
});
const HostPage = lazy(() => import("./pages/HostPage"));
return (
<>
<Header></Header>
<div class="container-lg">
<div class="row">
<div class="col-md mt-4 mb-4">
<Router>
<Route path="/" component={Body}/>
<Route path="/host/:id" component={HostPage}/>
</Router>
</div>
</div>
</div>
</>
)
}
export default App

View file

@ -0,0 +1,28 @@
import { Show } from "solid-js";
import { editNames, setEditNames } from "../../functions/exports";
import Filter from "../Filter";
import Search from "../Search";
function CardHead() {
return (
<div class="row">
<div class="col-md mt-1 mb-1">
<Filter></Filter>
</div>
<div class="col-md mt-1 mb-1">
<div class="d-flex justify-content-between">
<Search></Search>
<Show
when={editNames()}
fallback={<button class="btn btn-outline-primary" onClick={[setEditNames, true]}>Edit names</button>}
>
<button class="btn btn-primary" onClick={[setEditNames, false]}>Edit names</button>
</Show>
</div>
</div>
</div>
)
}
export default CardHead

View file

@ -0,0 +1,28 @@
import { Host } from "../../functions/exports";
import { sortByAnyField } from "../../functions/sort";
function TableHead() {
const handleSort = (sortBy: keyof Host) => {
sortByAnyField(sortBy);
};
return (
<thead>
<tr>
<th style="width: 3em;"></th>
<th>Name <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Name"]}></i></th>
<th>Iface <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Iface"]}></i></th>
<th>IP <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "IP"]}></i></th>
<th>MAC <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Mac"]}></i></th>
<th>Hardware <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Hw"]}></i></th>
<th>Date <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Date"]}></i></th>
<th>Known <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Known"]}></i></th>
<th>Online <i class="bi bi-sort-down-alt my-btn" onclick={[handleSort, "Now"]}></i></th>
<th style="width: 2em;"></th>
</tr>
</thead>
)
}
export default TableHead

View file

@ -0,0 +1,62 @@
import { createSignal, Show } from "solid-js";
import { editNames } from "../../functions/exports";
import { apiEditHost } from "../../functions/api";
function TableRow(_props: any) {
const [name, setName] = createSignal(_props.host.Name);
const link = "http://" + _props.host.IP;
const edit = "/host/" + _props.host.ID;
let now = <i class="bi bi-circle-fill" style="color:var(--bs-gray-500);"></i>;
if (_props.host.Now == 1) {
now = <i class="bi bi-check-circle-fill" style="color:var(--bs-success);"></i>;
};
let known = false;
if (_props.host.Known == 1) {
known = true;
}
const handleInput = async (n: string) => {
// console.log('Edit:', n);
setName(n);
await apiEditHost(_props.host.ID, name(), '');
};
const handleToggle = async () => {
await apiEditHost(_props.host.ID, name(), 'toggle');
};
return (
<tr>
<td class="opacity-50">{_props.index}.</td>
<td>
<Show
when={editNames()}
fallback={name()}
>
<input type="text" class="form-control" value={name()}
onInput={e => handleInput(e.target.value)}></input>
</Show>
</td>
<td>{_props.host.Iface}</td>
<td><a href={link} target="_blank">{_props.host.IP}</a></td>
<td>{_props.host.Mac}</td>
<td>{_props.host.Hw}</td>
<td>{_props.host.Date}</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" checked={known}
onClick={handleToggle}></input>
</div>
</td>
<td>{now}</td>
<td>
<a href={edit}><i class="bi bi-pencil-square my-btn"></i></a>
</td>
</tr>
)
}
export default TableRow

View file

@ -0,0 +1,38 @@
import { For } from "solid-js";
import { Host, ifaces } from "../functions/exports";
import { filterFunc } from "../functions/filter";
function Filter() {
const handleFilter = (field: keyof Host, value: any) => {
filterFunc(field, value);
};
return (
<div class="row">
<div class="col input-group">
<span class="input-group-text">Filter by</span>
<select class="form-select">
<option selected disabled>Iface</option>
<For each={ifaces()}>{(iface) =>
<option onClick={()=>{handleFilter("Iface", iface)}}>{iface}</option>
}</For>
</select>
<select class="form-select">
<option selected disabled>Known</option>
<option onClick={()=>{handleFilter("Known", 1)}}>Known</option>
<option onClick={()=>{handleFilter("Known", 0)}}>Unknown</option>
</select>
<select class="form-select">
<option selected disabled>Online</option>
<option onClick={()=>{handleFilter("Now", 1)}}>Online</option>
<option onClick={()=>{handleFilter("Now", 0)}}>Offline</option>
</select>
<button onClick={()=>{handleFilter("ID", 0)}} class="btn btn-outline-primary">Reset filter</button>
</div>
</div>
)
}
export default Filter

View file

@ -0,0 +1,73 @@
import { createSignal } from "solid-js";
import { Conf } from "../functions/exports";
function Header() {
const [themePath, setThemePath] = createSignal('');
const [iconsPath, setIconsPath] = createSignal('');
const setCurrentTheme = (appConfig:Conf) => {
const theme = appConfig.Theme?appConfig.Theme:"sand";
const color = appConfig.Color?appConfig.Color:"dark";
if (appConfig.NodePath == '') {
setThemePath("https://cdn.jsdelivr.net/npm/aceberg-bootswatch-fork@v5.3.3-2/dist/"+theme+"/bootstrap.min.css");
setIconsPath("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css");
} else {
setThemePath(appConfig.NodePath+"/node_modules/bootswatch/dist/"+theme+"/bootstrap.min.css");
setIconsPath(appConfig.NodePath+"/node_modules/bootstrap-icons/font/bootstrap-icons.css");
}
document.documentElement.setAttribute("data-bs-theme", color);
color === "dark"
? document.documentElement.style.setProperty('--transparent-light', '#ffffff15')
: document.documentElement.style.setProperty('--transparent-light', '#00000015');
}
setCurrentTheme({
Theme: "sand",
Color: "dark",
Timeout: 120,
NodePath: "",
});
return (
<>
<link rel="stylesheet" href={iconsPath()}></link> {/* icons */}
<link rel="stylesheet" href={themePath()}></link> {/* theme */}
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
<div class="container-lg">
<a class="navbar-brand" href="/">
<img src="/fs/public/favicon.png" style="width: 2em"/>
</a>
<ul class="navbar-nav">
<li class="nav-item">
<button class="btn navbar-toggler nav-link active fs-2" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-list"></i>
</button>
</li>
</ul>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link active" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link active" target="_self" href="/config/">Config</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/history/">History</a>
</li>
</ul>
</div>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active fs-3 ms-md-2" target="_blank" href="https://github.com/aceberg/WatchYourLAN"><i class="bi bi-github"></i></a>
</li>
</ul>
</div>
</nav>
</>
)
};
export default Header

View file

@ -0,0 +1,101 @@
import { apiDelHost, apiEditHost } from "../../functions/api";
import { currentHost } from "../../functions/exports";
function HostCard() {
let name:string = "";
const handleInput = async (n: string) => {
name = n;
await apiEditHost(currentHost().ID, name, '');
};
const handleToggle = async () => {
if (name == "") {
name = currentHost().Name;
}
await apiEditHost(currentHost().ID, name, 'toggle');
};
const handleDel = async () => {
await apiDelHost(currentHost().ID);
window.location.href = '/';
};
return (
<div class="card border-primary">
<div class="card-header">Host</div>
<div class="card-body table-responsive">
<table class="table table-striped table-hover">
<tbody>
<tr>
<td>ID</td>
<td>{currentHost().ID}</td>
</tr>
<tr>
<td>Name</td>
<td>
<input type="text" class="form-control" value={currentHost().Name}
onInput={e => handleInput(e.target.value)}></input>
</td>
</tr>
<tr>
<td>DNS name</td>
<td>{currentHost().DNS}</td>
</tr>
<tr>
<td>Iface</td>
<td>{currentHost().Iface}</td>
</tr>
<tr>
<td>IP</td>
<td>
<a href={"http://" + currentHost().IP} target="_blank">{currentHost().IP}</a>
</td>
</tr>
<tr>
<td>MAC</td>
<td>{currentHost().Mac}</td>
</tr>
<tr>
<td>Hardware</td>
<td>{currentHost().Hw}</td>
</tr>
<tr>
<td>Date</td>
<td>{currentHost().Date}</td>
</tr>
<tr>
<td>Known</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
onClick={handleToggle}
checked={currentHost().Known == 1
? true
: false
}
></input>
</div>
</td>
</tr>
<tr>
<td>Online</td>
<td>{currentHost().Now == 1
? <i class="bi bi-check-circle-fill" style="color:var(--bs-success);"></i>
: <i class="bi bi-circle-fill" style="color:var(--bs-gray-500);"></i>
}</td>
</tr>
</tbody>
</table>
<button type="button" onClick={handleDel} class="btn btn-outline-danger">Delete host</button>
</div>
</div>
)
}
export default HostCard

View file

@ -0,0 +1,78 @@
import { createSignal, For } from "solid-js";
import { apiPortScan } from "../../functions/api";
import { currentHost } from "../../functions/exports";
function Ping() {
let stop = false;
const [beginStr, setBegin] = createSignal("");
const [endStr, setEnd] = createSignal("");
const [curPort, setCurPort] = createSignal("");
const [foundPorts, setFoundPorts] = createSignal<number[]>([]);
const handleScan = async () => {
stop = false;
let begin = Number(beginStr());
if (Number.isNaN(begin) || begin < 1 || begin > 65535) {
begin = 1;
}
let end = Number(endStr());
if (Number.isNaN(end) || end < 1 || end > 65535) {
end = 65535;
}
let portOpened:boolean;
for (let i = begin ; i <= end; i++) {
if (stop) {
break;
}
setCurPort(i.toString());
portOpened = await apiPortScan(currentHost().IP, i);
if (portOpened) {
setFoundPorts([...foundPorts(), i]);
}
}
};
const handleStop = () => {
if (stop) {
setBegin(curPort());
handleScan();
} else {
stop = true;
}
}
return (
<div class="card border-primary">
<div class="card-header">Port Scan</div>
<div class="card-body">
<form class="input-group">
<input type="text" class="form-control" placeholder="1"
onInput={e => setBegin(e.target.value)}></input>
<input type="text" class="form-control" placeholder="65535"
onInput={e => setEnd(e.target.value)}></input>
<button type="button" onClick={handleScan} class="btn btn-primary">Scan</button>
</form>
{curPort() != ""
? <div class="d-flex justify-content-between mt-2">
<button type="button" onClick={handleStop} class="btn btn-warning">Stop/Continue</button>
<div>Scanning port: {curPort()}</div>
</div>
: <></>
}
<div class="mt-2">
<For each={foundPorts()}>{(port) =>
<a class="me-4" href={"http://" + currentHost().IP + ":" + port} target="_blank">{port}</a>
}</For>
</div>
</div>
</div>
)
}
export default Ping

View file

@ -0,0 +1,14 @@
import { searchFunc } from "../functions/search";
function Search() {
const handleSearch = (s: string) => {
searchFunc(s);
};
return (
<input onInput={e => handleSearch(e.target.value)} class="form-control" placeholder="Search" style="max-width: 10em;"></input>
)
}
export default Search

View file

@ -0,0 +1,40 @@
const api = 'http://0.0.0.0:8840';
export const apiGetAllHosts = async () => {
const url = api+'/api/all';
const hosts = await (await fetch(url)).json();
return hosts;
};
export const apiEditHost = async (id:number, name:string, known:string) => {
const url = api+'/api/edit/'+id+'/'+name+'/'+known;
const res = await (await fetch(url)).json();
return res;
};
export const apiGetHost = async (id:string) => {
const url = api+'/api/host/'+id;
const res = await (await fetch(url)).json();
return res;
};
export const apiDelHost = async (id:number) => {
const url = api+'/api/host/del/'+id;
const res = await (await fetch(url)).json();
return res;
};
export const apiPortScan = async (ip:string, port:number) => {
const url = api+'/api/port/'+ip+'/'+port;
const res = await (await fetch(url)).json();
return res;
};

View file

@ -0,0 +1,37 @@
import { apiGetAllHosts } from "./api";
import { allHosts, setAllHosts, setBkpHosts, setIfaces } from "./exports";
import { filterAtStart, filterFunc } from "./filter";
import { sortAtStart } from "./sort";
export function runAtStart() {
getHosts();
filterFunc("ID", 0); // reset filter
setInterval(() => {
getHosts();
}, 60000); // 60000 ms = 1 minute
}
async function getHosts() {
const hosts = await apiGetAllHosts();
setAllHosts(hosts);
setBkpHosts(hosts);
sortAtStart();
filterAtStart();
listIfaces();
// console.log("UPD");
}
function listIfaces() {
let ifaces:string[] = [];
for (let host of allHosts()) {
if (!ifaces.includes(host.Iface)) {
ifaces.push(host.Iface);
}
}
setIfaces(ifaces);
}

View file

@ -0,0 +1,46 @@
import { createSignal } from "solid-js";
export interface Host {
ID: number;
Name: string;
DNS: string;
Iface: string;
IP: string;
Mac: string;
Hw: string;
Date: string;
Known: number;
Now: number;
};
export const emptyHost:Host = {
ID: 0,
Name: "",
DNS: "",
Iface: "",
IP: "",
Mac: "",
Hw: "",
Date: "",
Known: 0,
Now: 0,
};
export interface Conf {
Theme: string;
Color: string;
Timeout: number;
NodePath: string;
};
export const [allHosts, setAllHosts] = createSignal<Host[]>([]);
export const [bkpHosts, setBkpHosts] = createSignal<Host[]>([]);
export const [ifaces, setIfaces] = createSignal<string[]>([]);
export const [appConfig, setAppConfig] = createSignal<Conf>();
export const [editNames, setEditNames] = createSignal(false);
export const [currentHost, setCurrentHost] = createSignal<Host>(emptyHost);

View file

@ -0,0 +1,40 @@
import { allHosts, bkpHosts, Host, setAllHosts } from "./exports";
let oldFilter = 'ID';
export function filterAtStart() {
const field = localStorage.getItem("filterField") as keyof Host;
const value = localStorage.getItem("filterValue");
filterFunc(field, value);
}
export function filterFunc(field: keyof Host, value: any) {
let addrsArray = allHosts();
if (oldFilter == field) {
addrsArray = bkpHosts();
}
oldFilter = field;
localStorage.setItem("filterField", field);
localStorage.setItem("filterValue", value);
switch (field) {
case 'Iface':
addrsArray = addrsArray.filter((item) => item.Iface == value);
break;
case 'Known':
addrsArray = addrsArray.filter((item) => item.Known == value);
break;
case 'Now':
addrsArray = addrsArray.filter((item) => item.Now == value);
break;
default:
addrsArray = bkpHosts();
}
setAllHosts([]);
setAllHosts(addrsArray);
}

View file

@ -0,0 +1,37 @@
import { allHosts, bkpHosts, Host, setAllHosts } from "./exports";
export function searchFunc(s: string) {
if (s != "") {
const sl = s.toLowerCase();
let newArray:Host[] = [];
for (let item of allHosts()) {
if (searchItem(item, sl)) {
newArray.push(item);
}
}
setAllHosts([]);
setAllHosts(newArray);
} else {
setAllHosts([]);
setAllHosts(bkpHosts());
}
}
function searchItem(item:Host, sl:string) {
const name = item.Name.toLowerCase();
const hw = item.Hw.toLowerCase();
const mac = item.Mac.toLowerCase();
if ((name.includes(sl)) || (item.Iface.includes(sl)) || (item.IP.includes(sl)) || (mac.includes(sl)) || (hw.includes(sl)) || (item.Date.includes(sl))) {
return true;
} else {
return false;
}
}

View file

@ -0,0 +1,59 @@
import { allHosts, Host, setAllHosts } from "./exports";
let down = false;
let oldField = '';
export function sortAtStart() {
const field = localStorage.getItem("sortField") as keyof Host;
down = JSON.parse(localStorage.getItem("sortDown") as string);
down = !down;
sortByAnyField(field);
}
export function sortByAnyField(field: keyof Host) {
if (field != oldField) {
oldField = field;
down = !down;
} else {
oldField = '';
down = !down;
}
localStorage.setItem("sortDown", down.toString());
localStorage.setItem("sortField", field);
// checkNotEmpty(addrsArray);
let someArray = allHosts();
if (field == 'IP') {
someArray.sort((a, b) => sortIP(a, b, down));
} else {
someArray.sort((a, b) => byField(a, b, field, down));
}
setAllHosts([]);
setAllHosts(someArray);
}
function byField(a:Host, b:Host, fieldName: keyof Host, down:boolean){
if (a[fieldName] > b[fieldName]) {
return down ? 1 : -1;
} else {
return !down ? 1 : -1;
}
}
function sortIP(a:Host, b:Host, down: boolean) {
const num1 = numIP(a);
const num2 = numIP(b);
if (down) {
return num1-num2;
} else {
return num2-num1;
}
}
function numIP(a:Host) {
return Number(a.IP.split(".").map((num) => (`000${num}`).slice(-3) ).join(""));
}

7
frontend/src/index.tsx Normal file
View file

@ -0,0 +1,7 @@
/* @refresh reload */
import { render } from 'solid-js/web'
import App from './App.tsx'
const root = document.getElementById('root')
render(() => <App />, root!)

View file

@ -0,0 +1,30 @@
import { For } from "solid-js";
import { allHosts } from "../functions/exports";
import TableRow from "../components/Body/TableRow";
import TableHead from "../components/Body/TableHead";
import CardHead from "../components/Body/CardHead";
function Body() {
return (
<div class="card border-primary">
<div class="card-header">
<CardHead></CardHead>
</div>
<div class="card-body table-responsive">
<table class="table table-striped table-hover">
<TableHead></TableHead>
<tbody>
<For each={allHosts()}>{(host, index) =>
<TableRow host={host} index={index}></TableRow>
}</For>
</tbody>
</table>
</div>
</div>
)
}
export default Body

View file

@ -0,0 +1,31 @@
import { useParams } from "@solidjs/router";
import { onMount } from "solid-js";
import { apiGetHost } from "../functions/api";
import { setCurrentHost } from "../functions/exports";
import HostCard from "../components/HostPage/HostCard";
import Ping from "../components/HostPage/Ping";
function HostPage() {
onMount(async () => {
const params = useParams();
const host = await apiGetHost(params.id);
setCurrentHost(host);
});
return (
<div class="row">
<div class="col-md">
<HostCard></HostCard>
</div>
<div class="col-md">
<Ping></Ping>
</div>
</div>
)
}
export default HostPage

1
frontend/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
}

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

6
frontend/vite.config.ts Normal file
View file

@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})

14
go.mod
View file

@ -1,6 +1,6 @@
module github.com/aceberg/WatchYourLAN
go 1.23.1
go 1.23.6
require (
github.com/containrrr/shoutrrr v0.8.0
@ -10,7 +10,7 @@ require (
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.21.1
github.com/spf13/viper v1.19.0
modernc.org/sqlite v1.33.1
modernc.org/sqlite v1.36.1
)
require (
@ -31,7 +31,6 @@ require (
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -72,10 +71,7 @@ require (
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
)

44
go.sum
View file

@ -61,8 +61,6 @@ github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlG
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
@ -205,30 +203,28 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View file

@ -43,6 +43,7 @@ func apiHost(c *gin.Context) {
idStr := c.Param("id")
host := getHostByID(idStr, allHosts) // functions.go
_, host.DNS = updateDNS(host)
c.IndentedJSON(http.StatusOK, host)
}

View file

@ -1,22 +0,0 @@
package web
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func hostHandler(c *gin.Context) {
var guiData models.GuiData
guiData.Config = appConfig
idStr := c.Param("id")
host := getHostByID(idStr, allHosts)
_, host.DNS = updateDNS(host)
guiData.Host = host
c.HTML(http.StatusOK, "header.html", guiData)
c.HTML(http.StatusOK, "host.html", guiData)
}

View file

@ -13,7 +13,6 @@ func indexHandler(c *gin.Context) {
guiData.Config = appConfig
guiData.Themes = getAllIfaces(allHosts)
c.HTML(http.StatusOK, "header.html", guiData)
c.HTML(http.StatusOK, "index.html", guiData)
}
@ -21,6 +20,5 @@ func historyHandler(c *gin.Context) {
var guiData models.GuiData
guiData.Config = appConfig
c.HTML(http.StatusOK, "header.html", guiData)
c.HTML(http.StatusOK, "history.html", guiData)
}

View file

@ -0,0 +1 @@
import{t as o,i as l,c as t,a as K,b as x,s as O,d as A,e as et,f as B,g as v,h as I,F as lt,j as it,o as nt,u as rt,k as at,l as st}from"./index.js";var dt=o('<div class="card border-primary"><div class=card-header>Host</div><div class="card-body table-responsive"><table class="table table-striped table-hover"><tbody><tr><td>ID</td><td></td></tr><tr><td>Name</td><td><input type=text class=form-control></td></tr><tr><td>DNS name</td><td></td></tr><tr><td>Iface</td><td></td></tr><tr><td>IP</td><td><a target=_blank></a></td></tr><tr><td>MAC</td><td></td></tr><tr><td>Hardware</td><td></td></tr><tr><td>Date</td><td></td></tr><tr><td>Known</td><td><div class="form-check form-switch"><input class=form-check-input type=checkbox></div></td></tr><tr><td>Online</td><td></td></tr></tbody></table><button type=button class="btn btn-outline-danger">Delete host'),ct=o('<i class="bi bi-check-circle-fill"style=color:var(--bs-success);>'),ot=o('<i class="bi bi-circle-fill"style=color:var(--bs-gray-500);>');function $t(){let e="";const s=async b=>{e=b,await A(t().ID,e,"")},$=async()=>{e==""&&(e=t().Name),await A(t().ID,e,"toggle")},C=async()=>{await et(t().ID),window.location.href="/"};return(()=>{var b=dt(),_=b.firstChild,y=_.nextSibling,u=y.firstChild,w=u.firstChild,f=w.firstChild,k=f.firstChild,n=k.nextSibling,r=f.nextSibling,c=r.firstChild,a=c.nextSibling,g=a.firstChild,h=r.nextSibling,N=h.firstChild,S=N.nextSibling,i=h.nextSibling,d=i.firstChild,m=d.nextSibling,p=i.nextSibling,G=p.firstChild,R=G.nextSibling,D=R.firstChild,H=p.nextSibling,T=H.firstChild,q=T.nextSibling,E=H.nextSibling,z=E.firstChild,J=z.nextSibling,M=E.nextSibling,L=M.firstChild,Q=L.nextSibling,F=M.nextSibling,U=F.firstChild,V=U.nextSibling,W=V.firstChild,j=W.firstChild,X=F.nextSibling,Y=X.firstChild,Z=Y.nextSibling,tt=u.nextSibling;return l(n,()=>t().ID),g.$$input=P=>s(P.target.value),l(S,()=>t().DNS),l(m,()=>t().Iface),l(D,()=>t().IP),l(q,()=>t().Mac),l(J,()=>t().Hw),l(Q,()=>t().Date),j.$$click=$,l(Z,(()=>{var P=K(()=>t().Now==1);return()=>P()?ct():ot()})()),tt.$$click=C,x(()=>O(D,"href","http://"+t().IP)),x(()=>g.value=t().Name),x(()=>j.checked=t().Known==1),b})()}B(["input","click"]);var bt=o('<div class="card border-primary"><div class=card-header>Port Scan</div><div class=card-body><form class=input-group><input type=text class=form-control placeholder=1><input type=text class=form-control placeholder=65535><button type=button class="btn btn-primary">Scan</button></form><div class=mt-2>'),_t=o('<div class="d-flex justify-content-between mt-2"><button type=button class="btn btn-warning">Stop/Continue</button><div>Scanning port: '),ut=o("<a class=me-4 target=_blank>");function ft(){let e=!1;const[s,$]=v(""),[C,b]=v(""),[_,y]=v(""),[u,w]=v([]),f=async()=>{e=!1;let n=Number(s());(Number.isNaN(n)||n<1||n>65535)&&(n=1);let r=Number(C());(Number.isNaN(r)||r<1||r>65535)&&(r=65535);let c;for(let a=n;a<=r&&!e;a++)y(a.toString()),c=await it(t().IP,a),c&&w([...u(),a])},k=()=>{e?($(_()),f()):e=!0};return(()=>{var n=bt(),r=n.firstChild,c=r.nextSibling,a=c.firstChild,g=a.firstChild,h=g.nextSibling,N=h.nextSibling,S=a.nextSibling;return g.$$input=i=>$(i.target.value),h.$$input=i=>b(i.target.value),N.$$click=f,l(c,(()=>{var i=K(()=>_()!="");return()=>i()?(()=>{var d=_t(),m=d.firstChild,p=m.nextSibling;return p.firstChild,m.$$click=k,l(p,_,null),d})():[]})(),S),l(S,I(lt,{get each(){return u()},children:i=>(()=>{var d=ut();return l(d,i),x(()=>O(d,"href","http://"+t().IP+":"+i)),d})()})),n})()}B(["input","click"]);var gt=o("<div class=row><div class=col-md></div><div class=col-md>");function pt(){return nt(async()=>{const e=rt(),s=await at(e.id);st(s)}),(()=>{var e=gt(),s=e.firstChild,$=s.nextSibling;return l(s,I($t,{})),l($,I(ft,{})),e})()}export{pt as default};

View file

@ -0,0 +1 @@
.my-btn{height:100%;background-color:#0000;color:var(--bs-primary);text-align:center}.my-btn:hover{background-color:#0000001a}

File diff suppressed because one or more lines are too long

View file

@ -20,8 +20,6 @@
<link href="{{ .Config.NodePath }}/node_modules/bootswatch/dist/{{ .Config.Theme }}/bootstrap.min.css" rel="stylesheet">
<script src="{{ .Config.NodePath }}/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
{{ end }}
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="/fs/public/css/index.css" />
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
@ -42,10 +40,10 @@
<a class="nav-link active" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/config/">Config</a>
<a class="nav-link active" target="_self" href="/config/">Config</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/history/">History</a>
<a class="nav-link active" target="_self" href="/history/">History</a>
</li>
</ul>
</div>

View file

@ -1,73 +1,19 @@
{{ define "index.html" }}
</head>
<body>
<div class="container-lg">
<div class="row">
<div class="col-md mt-2">
<div class="form-check form-switch">
<input type="hidden" id="ref-timeout" value="{{ .Config.Timeout }}">
<input class="form-check-input" type="checkbox" id="ref" onclick="toggleRefresh()" title="Auto refresh">
</div>
</div>
</div>
<div class="row">
<div class="col-md mt-2 mb-4">
<div class="card border-primary">
<div class="card-header">
<div class="row">
<div class="col-md mt-1 mb-1">
<button class="btn btn-outline-primary" disabled>Filter by</button>
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Iface</button>
<ul class="dropdown-menu">
{{ range .Themes }}
<li><a class="dropdown-item" onclick="filterFunc('iface', '{{ . }}')">{{ . }}</a></li>
{{ end }}
</ul>
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Known</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" onclick="filterFunc('known', 1)">Known</a></li>
<li><a class="dropdown-item" onclick="filterFunc('known', 0)">Unknown</a></li>
</ul>
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Online</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" onclick="filterFunc('line', 1)">Online</a></li>
<li><a class="dropdown-item" onclick="filterFunc('line', 0)">Offline</a></li>
</ul>
<button class="btn btn-outline-primary" onclick="resetFilter()">Reset filter</button>
</div>
<div class="col-md mt-1 mb-1">
<div class="d-flex justify-content-between">
<input class="form-control" id="search" oninput="searchFunc()" placeholder="Search" style="max-width: 10em;">
<button class="btn btn-outline-primary" id="edit" onclick="editClick()">Edit names</button>
</div>
</div>
</div>
</div>
<div class="card-body table-responsive">
<table class="table table-striped">
<thead>
<th style="width: 3em;"></th>
<th>Name <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Name')"></i></th>
<th>Iface <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Iface')"></i></th>
<th>IP <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('IP')"></i></th>
<th>MAC <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Mac')"></i></th>
<th>Hardware <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Hw')"></i></th>
<th>Date <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Date')"></i></th>
<th>Known <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Known')"></i></th>
<th>Online <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Now')"></i></th>
</thead>
<tbody id="tBody"></tbody>
<!-- index.js -->
</table>
</div>
</div>
</div>
</div>
</div>
<script src="/fs/public/js/index.js"></script>
<script src="/fs/public/js/sort.js"></script>
<script src="/fs/public/js/filter-search.js"></script>
<script src="/fs/public/js/refresh.js"></script>
{{ template "footer.html" }}
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<title>WatchYourLAN</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--Favicon-->
<link rel="icon" type="image/x-icon" href="/fs/public/favicon.png">
<!-- JS & CSS -->
<script type="module" crossorigin src="/fs/public/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/fs/public/assets/index.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
{{ end }}

View file

@ -0,0 +1,73 @@
{{ define "oldindex.html" }}
</head>
<body>
<div class="container-lg">
<div class="row">
<div class="col-md mt-2">
<div class="form-check form-switch">
<input type="hidden" id="ref-timeout" value="{{ .Config.Timeout }}">
<input class="form-check-input" type="checkbox" id="ref" onclick="toggleRefresh()" title="Auto refresh">
</div>
</div>
</div>
<div class="row">
<div class="col-md mt-2 mb-4">
<div class="card border-primary">
<div class="card-header">
<div class="row">
<div class="col-md mt-1 mb-1">
<button class="btn btn-outline-primary" disabled>Filter by</button>
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Iface</button>
<ul class="dropdown-menu">
{{ range .Themes }}
<li><a class="dropdown-item" onclick="filterFunc('iface', '{{ . }}')">{{ . }}</a></li>
{{ end }}
</ul>
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Known</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" onclick="filterFunc('known', 1)">Known</a></li>
<li><a class="dropdown-item" onclick="filterFunc('known', 0)">Unknown</a></li>
</ul>
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Online</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" onclick="filterFunc('line', 1)">Online</a></li>
<li><a class="dropdown-item" onclick="filterFunc('line', 0)">Offline</a></li>
</ul>
<button class="btn btn-outline-primary" onclick="resetFilter()">Reset filter</button>
</div>
<div class="col-md mt-1 mb-1">
<div class="d-flex justify-content-between">
<input class="form-control" id="search" oninput="searchFunc()" placeholder="Search" style="max-width: 10em;">
<button class="btn btn-outline-primary" id="edit" onclick="editClick()">Edit names</button>
</div>
</div>
</div>
</div>
<div class="card-body table-responsive">
<table class="table table-striped">
<thead>
<th style="width: 3em;"></th>
<th>Name <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Name')"></i></th>
<th>Iface <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Iface')"></i></th>
<th>IP <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('IP')"></i></th>
<th>MAC <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Mac')"></i></th>
<th>Hardware <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Hw')"></i></th>
<th>Date <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Date')"></i></th>
<th>Known <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Known')"></i></th>
<th>Online <i class="bi bi-sort-down-alt my-btn" onclick="sortBy('Now')"></i></th>
</thead>
<tbody id="tBody"></tbody>
<!-- index.js -->
</table>
</div>
</div>
</div>
</div>
</div>
<script src="/fs/public/js/index.js"></script>
<script src="/fs/public/js/sort.js"></script>
<script src="/fs/public/js/filter-search.js"></script>
<script src="/fs/public/js/refresh.js"></script>
{{ template "footer.html" }}
{{ end }}

View file

@ -57,7 +57,7 @@ func Gui(dirPath, nodePath string) {
router.GET("/", indexHandler) // index.go
router.GET("/history/", historyHandler) // index.go
router.GET("/host/:id", hostHandler) // host.go
// router.GET("/host/:id", hostHandler) // host.go
router.GET("/config/", configHandler) // config.go
router.POST("/config/", saveConfigHandler) // config.go