Compare commits

..

10 commits

Author SHA1 Message Date
aceberg
ca9f644bcc Release 2.1.4 (#72,#74,#135,#195,#196) 2025-09-10 19:06:56 +07:00
aceberg
847b4caaf8 Fix WOL button 2025-09-10 18:30:21 +07:00
aceberg
24ee0acb45 Add host from API, trigger rescan 2025-09-07 03:33:42 +07:00
aceberg
1271b7b42e Delete selected hosts 2025-09-07 01:24:15 +07:00
aceberg
92e9a80770 About card upd 2025-09-06 03:15:53 +07:00
aceberg
c615198560 Swagger API docs 2025-09-06 01:23:39 +07:00
aceberg
f1bb8166ba Move backend to backend dir 2025-09-05 06:10:16 +07:00
aceberg
dee79fc74f Wake-on-LAN frontend 2025-09-05 02:40:00 +07:00
aceberg
6781adb430 Wake-on-LAN 2025-09-05 02:10:58 +07:00
aceberg
2544a3f390 README upd 2025-08-05 04:03:31 +07:00
76 changed files with 2425 additions and 319 deletions

View file

@ -8,7 +8,7 @@ on:
jobs:
generate:
name: Create release-artifacts
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout the repository
uses: actions/checkout@master
@ -21,7 +21,8 @@ jobs:
- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
version: latest
args: release --clean
workdir: backend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -13,7 +13,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout repository

View file

@ -13,7 +13,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout repository

View file

@ -1 +1 @@
internal/web/public/version
backend/internal/web/public/version

View file

@ -1,6 +1,14 @@
# Change Log
All notable changes to this project will be documented in this file.
## [v2.1.4] - 2025-09-10
### Added
- Swagger API docs (`/swagger/index.html`)
- Add host from API [#72](https://github.com/aceberg/WatchYourLAN/issues/72)
- Trigger rescan from API or by pressing `Save` on `Config/Scan settings` [#74](https://github.com/aceberg/WatchYourLAN/issues/74)
- Delete selected hosts [#195](https://github.com/aceberg/WatchYourLAN/issues/195)
- Wake-on-LAN [#135](https://github.com/aceberg/WatchYourLAN/issues/135), [#196](https://github.com/aceberg/WatchYourLAN/issues/196)
## [v2.1.3] - 2025-07-26
### Fixed
- Memory leak bug [#149](https://github.com/aceberg/WatchYourLAN/issues/149)

View file

@ -6,10 +6,10 @@ COPY --from=xx / /
WORKDIR /src
COPY go.mod go.sum ./
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY . .
COPY backend/ .
ARG TARGETPLATFORM
RUN CGO_ENABLED=0 xx-go build -ldflags='-w -s' -o /WatchYourLAN ./cmd/WatchYourLAN

View file

@ -6,9 +6,9 @@
[![Docker](https://github.com/aceberg/WatchYourLAN/actions/workflows/main-docker-all.yml/badge.svg)](https://github.com/aceberg/WatchYourLAN/actions/workflows/main-docker-all.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/aceberg/WatchYourLAN)](https://goreportcard.com/report/github.com/aceberg/WatchYourLAN)
[![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/aceberg/watchyourlan)](https://hub.docker.com/r/aceberg/watchyourlan)
[![GitHub Discussions](https://img.shields.io/github/discussions/aceberg/WatchYourLAN)](https://github.com/aceberg/WatchYourLAN/discussions)
[![GitHub Discussions](https://img.shields.io/github/discussions/aceberg/WatchYourLAN)](https://github.com/aceberg/WatchYourLAN/discussions)
<a href="https://trendshift.io/repositories/11642" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11642" alt="aceberg%2FWatchYourLAN | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
Lightweight network IP scanner with web GUI. Features:
- Send notification when new host is found

View file

@ -54,8 +54,6 @@ nfpms:
archives:
- files:
- LICENSE
- README.md
- CHANGELOG.md
- src: ./configs/watchyourlan.service
dst: watchyourlan.service
- src: ./configs/install.sh

21
backend/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 aceberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -7,7 +7,7 @@ mod:
run:
cd cmd/WatchYourLAN/ && \
sudo \
go run . #-c /data/config #-n http://192.168.2.3:8850
go run . #-n http://192.168.2.3:8850
fmt:
go fmt ./...
@ -16,4 +16,7 @@ lint:
golangci-lint run
golint ./...
check: fmt lint
check: fmt lint
swag:
swag init -g main.go --dir cmd/WatchYourLAN,internal/api,internal/models

View file

@ -1,3 +1,11 @@
// @title WatchYourLAN API
// @version 0.1
// @description Lightweight network IP scanner written in Go
// @contact.url https://github.com/aceberg/WatchYourLAN
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @BasePath /api/
package main
import (
@ -6,6 +14,9 @@ import (
// _ "net/http/pprof"
// Import Swagger docs
_ "github.com/aceberg/WatchYourLAN/docs"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/gdb"
"github.com/aceberg/WatchYourLAN/internal/routines"

630
backend/docs/docs.go Normal file
View file

@ -0,0 +1,630 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {
"url": "https://github.com/aceberg/WatchYourLAN"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/all": {
"get": {
"description": "Retrieve all hosts from the database",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Get all hosts",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/config": {
"get": {
"description": "Returns the current configuration used by the app",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Get application configuration",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Conf"
}
}
}
}
},
"/edit/{id}/{name}/{known}": {
"get": {
"description": "Update a host's name and optionally toggle its \"known\" status",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Edit host",
"parameters": [
{
"type": "string",
"description": "Host ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "New name for the host",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Pass 'toggle' to flip the known/unknown status",
"name": "known",
"in": "path"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/history": {
"get": {
"description": "Retrieve the complete history of all hosts. Not recommended, the output can be a lot",
"produces": [
"application/json"
],
"tags": [
"history"
],
"summary": "Get full history",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/history/{mac}": {
"get": {
"description": "Retrieve the latest history entries for a specific host by MAC address",
"produces": [
"application/json"
],
"tags": [
"history"
],
"summary": "Get history by MAC",
"parameters": [
{
"type": "string",
"description": "MAC address of the host",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Number of history entries to return",
"name": "num",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/history/{mac}/{date}": {
"get": {
"description": "Retrieve history for a specific host on a given date\nThe date format is flexible and can be:\n- Year only: ` + "`" + `2025` + "`" + `\n- Year + month: ` + "`" + `2025-09` + "`" + `\n- Full date: ` + "`" + `2025-09-06` + "`" + `\n- Full timestamp: ` + "`" + `2025-09-06 00:58:26` + "`" + `",
"produces": [
"application/json"
],
"tags": [
"history"
],
"summary": "Get history by date",
"parameters": [
{
"type": "string",
"description": "MAC address of the host",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Date filter (supports YYYY, YYYY-MM, YYYY-MM-DD, YYYY-MM-DD HH:mm:ss)",
"name": "date",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/host/add/{mac}": {
"get": {
"description": "Add host by MAC, with optional Name, IP, Hardware\nReturns ` + "`" + `models.Host` + "`" + ` with this MAC form DB, either just added or existing",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Add host manually",
"parameters": [
{
"type": "string",
"description": "Host MAC",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "IP",
"name": "ip",
"in": "query"
},
{
"type": "string",
"description": "Hardware",
"name": "hw",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Host"
}
}
}
}
},
"/host/del/{id}": {
"get": {
"description": "Remove a host from the database by its unique ID",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Delete host",
"parameters": [
{
"type": "string",
"description": "Host ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/host/{id}": {
"get": {
"description": "Retrieve detailed information about a host by its unique ID",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Get host by ID",
"parameters": [
{
"type": "string",
"description": "Host ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Host"
}
}
}
}
},
"/notify_test": {
"get": {
"description": "Trigger a test notification to verify notification settings",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Send test notification",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/port/{addr}/{port}": {
"get": {
"description": "Check whether a given TCP port on an address is open or closed",
"produces": [
"application/json"
],
"tags": [
"network"
],
"summary": "Check port state",
"parameters": [
{
"type": "string",
"description": "IP address or hostname",
"name": "addr",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Port number",
"name": "port",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "true if open, false if closed",
"schema": {
"type": "boolean"
}
}
}
}
},
"/rescan": {
"get": {
"description": "Manually trigger rescan",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Rescan all interfaces now",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/status/{iface}": {
"get": {
"description": "Retrieve summary statistics of hosts, optionally filtered by interface",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Get network status",
"parameters": [
{
"type": "string",
"description": "Interface name (omit for all interfaces)",
"name": "iface",
"in": "path"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Stat"
}
}
}
}
},
"/version": {
"get": {
"description": "Returns the current running version of the application",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Get application version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/wol/{mac}": {
"get": {
"description": "Send a magic packet to wake up a host by its MAC address",
"produces": [
"application/json"
],
"tags": [
"network"
],
"summary": "Send Wake-on-LAN packet",
"parameters": [
{
"type": "string",
"description": "MAC address of the host",
"name": "mac",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "true if sent successfully",
"schema": {
"type": "boolean"
}
}
}
}
}
},
"definitions": {
"models.Conf": {
"type": "object",
"properties": {
"arpArgs": {
"type": "string"
},
"arpStrs": {
"type": "array",
"items": {
"type": "string"
}
},
"color": {
"type": "string"
},
"confPath": {
"type": "string"
},
"dbpath": {
"type": "string"
},
"dirPath": {
"type": "string"
},
"host": {
"type": "string"
},
"ifaces": {
"type": "string"
},
"influxAddr": {
"type": "string"
},
"influxBucket": {
"type": "string"
},
"influxEnable": {
"description": "InfluxDB",
"type": "boolean"
},
"influxOrg": {
"type": "string"
},
"influxSkipTLS": {
"type": "boolean"
},
"influxToken": {
"type": "string"
},
"logLevel": {
"type": "string"
},
"nodePath": {
"type": "string"
},
"pgconnect": {
"type": "string"
},
"port": {
"type": "string"
},
"prometheusEnable": {
"description": "Prometheus",
"type": "boolean"
},
"shoutURL": {
"type": "string"
},
"theme": {
"type": "string"
},
"timeout": {
"type": "integer"
},
"trimHist": {
"type": "integer"
},
"useDB": {
"description": "PostgreSQL",
"type": "string"
},
"version": {
"type": "string"
}
}
},
"models.Host": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"dns": {
"type": "string"
},
"hw": {
"type": "string"
},
"id": {
"type": "integer"
},
"iface": {
"type": "string"
},
"ip": {
"type": "string"
},
"known": {
"type": "integer"
},
"mac": {
"type": "string"
},
"name": {
"type": "string"
},
"now": {
"type": "integer"
}
}
},
"models.Stat": {
"type": "object",
"properties": {
"known": {
"type": "integer"
},
"offline": {
"type": "integer"
},
"online": {
"type": "integer"
},
"total": {
"type": "integer"
},
"unknown": {
"type": "integer"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "0.1",
Host: "",
BasePath: "/api/",
Schemes: []string{},
Title: "WatchYourLAN API",
Description: "Lightweight network IP scanner written in Go",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

605
backend/docs/swagger.json Normal file
View file

@ -0,0 +1,605 @@
{
"swagger": "2.0",
"info": {
"description": "Lightweight network IP scanner written in Go",
"title": "WatchYourLAN API",
"contact": {
"url": "https://github.com/aceberg/WatchYourLAN"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"version": "0.1"
},
"basePath": "/api/",
"paths": {
"/all": {
"get": {
"description": "Retrieve all hosts from the database",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Get all hosts",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/config": {
"get": {
"description": "Returns the current configuration used by the app",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Get application configuration",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Conf"
}
}
}
}
},
"/edit/{id}/{name}/{known}": {
"get": {
"description": "Update a host's name and optionally toggle its \"known\" status",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Edit host",
"parameters": [
{
"type": "string",
"description": "Host ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "New name for the host",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Pass 'toggle' to flip the known/unknown status",
"name": "known",
"in": "path"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/history": {
"get": {
"description": "Retrieve the complete history of all hosts. Not recommended, the output can be a lot",
"produces": [
"application/json"
],
"tags": [
"history"
],
"summary": "Get full history",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/history/{mac}": {
"get": {
"description": "Retrieve the latest history entries for a specific host by MAC address",
"produces": [
"application/json"
],
"tags": [
"history"
],
"summary": "Get history by MAC",
"parameters": [
{
"type": "string",
"description": "MAC address of the host",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Number of history entries to return",
"name": "num",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/history/{mac}/{date}": {
"get": {
"description": "Retrieve history for a specific host on a given date\nThe date format is flexible and can be:\n- Year only: `2025`\n- Year + month: `2025-09`\n- Full date: `2025-09-06`\n- Full timestamp: `2025-09-06 00:58:26`",
"produces": [
"application/json"
],
"tags": [
"history"
],
"summary": "Get history by date",
"parameters": [
{
"type": "string",
"description": "MAC address of the host",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Date filter (supports YYYY, YYYY-MM, YYYY-MM-DD, YYYY-MM-DD HH:mm:ss)",
"name": "date",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Host"
}
}
}
}
}
},
"/host/add/{mac}": {
"get": {
"description": "Add host by MAC, with optional Name, IP, Hardware\nReturns `models.Host` with this MAC form DB, either just added or existing",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Add host manually",
"parameters": [
{
"type": "string",
"description": "Host MAC",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "IP",
"name": "ip",
"in": "query"
},
{
"type": "string",
"description": "Hardware",
"name": "hw",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Host"
}
}
}
}
},
"/host/del/{id}": {
"get": {
"description": "Remove a host from the database by its unique ID",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Delete host",
"parameters": [
{
"type": "string",
"description": "Host ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/host/{id}": {
"get": {
"description": "Retrieve detailed information about a host by its unique ID",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Get host by ID",
"parameters": [
{
"type": "string",
"description": "Host ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Host"
}
}
}
}
},
"/notify_test": {
"get": {
"description": "Trigger a test notification to verify notification settings",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Send test notification",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/port/{addr}/{port}": {
"get": {
"description": "Check whether a given TCP port on an address is open or closed",
"produces": [
"application/json"
],
"tags": [
"network"
],
"summary": "Check port state",
"parameters": [
{
"type": "string",
"description": "IP address or hostname",
"name": "addr",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Port number",
"name": "port",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "true if open, false if closed",
"schema": {
"type": "boolean"
}
}
}
}
},
"/rescan": {
"get": {
"description": "Manually trigger rescan",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Rescan all interfaces now",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/status/{iface}": {
"get": {
"description": "Retrieve summary statistics of hosts, optionally filtered by interface",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Get network status",
"parameters": [
{
"type": "string",
"description": "Interface name (omit for all interfaces)",
"name": "iface",
"in": "path"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Stat"
}
}
}
}
},
"/version": {
"get": {
"description": "Returns the current running version of the application",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Get application version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/wol/{mac}": {
"get": {
"description": "Send a magic packet to wake up a host by its MAC address",
"produces": [
"application/json"
],
"tags": [
"network"
],
"summary": "Send Wake-on-LAN packet",
"parameters": [
{
"type": "string",
"description": "MAC address of the host",
"name": "mac",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "true if sent successfully",
"schema": {
"type": "boolean"
}
}
}
}
}
},
"definitions": {
"models.Conf": {
"type": "object",
"properties": {
"arpArgs": {
"type": "string"
},
"arpStrs": {
"type": "array",
"items": {
"type": "string"
}
},
"color": {
"type": "string"
},
"confPath": {
"type": "string"
},
"dbpath": {
"type": "string"
},
"dirPath": {
"type": "string"
},
"host": {
"type": "string"
},
"ifaces": {
"type": "string"
},
"influxAddr": {
"type": "string"
},
"influxBucket": {
"type": "string"
},
"influxEnable": {
"description": "InfluxDB",
"type": "boolean"
},
"influxOrg": {
"type": "string"
},
"influxSkipTLS": {
"type": "boolean"
},
"influxToken": {
"type": "string"
},
"logLevel": {
"type": "string"
},
"nodePath": {
"type": "string"
},
"pgconnect": {
"type": "string"
},
"port": {
"type": "string"
},
"prometheusEnable": {
"description": "Prometheus",
"type": "boolean"
},
"shoutURL": {
"type": "string"
},
"theme": {
"type": "string"
},
"timeout": {
"type": "integer"
},
"trimHist": {
"type": "integer"
},
"useDB": {
"description": "PostgreSQL",
"type": "string"
},
"version": {
"type": "string"
}
}
},
"models.Host": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"dns": {
"type": "string"
},
"hw": {
"type": "string"
},
"id": {
"type": "integer"
},
"iface": {
"type": "string"
},
"ip": {
"type": "string"
},
"known": {
"type": "integer"
},
"mac": {
"type": "string"
},
"name": {
"type": "string"
},
"now": {
"type": "integer"
}
}
},
"models.Stat": {
"type": "object",
"properties": {
"known": {
"type": "integer"
},
"offline": {
"type": "integer"
},
"online": {
"type": "integer"
},
"total": {
"type": "integer"
},
"unknown": {
"type": "integer"
}
}
}
}
}

409
backend/docs/swagger.yaml Normal file
View file

@ -0,0 +1,409 @@
basePath: /api/
definitions:
models.Conf:
properties:
arpArgs:
type: string
arpStrs:
items:
type: string
type: array
color:
type: string
confPath:
type: string
dbpath:
type: string
dirPath:
type: string
host:
type: string
ifaces:
type: string
influxAddr:
type: string
influxBucket:
type: string
influxEnable:
description: InfluxDB
type: boolean
influxOrg:
type: string
influxSkipTLS:
type: boolean
influxToken:
type: string
logLevel:
type: string
nodePath:
type: string
pgconnect:
type: string
port:
type: string
prometheusEnable:
description: Prometheus
type: boolean
shoutURL:
type: string
theme:
type: string
timeout:
type: integer
trimHist:
type: integer
useDB:
description: PostgreSQL
type: string
version:
type: string
type: object
models.Host:
properties:
date:
type: string
dns:
type: string
hw:
type: string
id:
type: integer
iface:
type: string
ip:
type: string
known:
type: integer
mac:
type: string
name:
type: string
now:
type: integer
type: object
models.Stat:
properties:
known:
type: integer
offline:
type: integer
online:
type: integer
total:
type: integer
unknown:
type: integer
type: object
info:
contact:
url: https://github.com/aceberg/WatchYourLAN
description: Lightweight network IP scanner written in Go
license:
name: MIT
url: https://opensource.org/licenses/MIT
title: WatchYourLAN API
version: "0.1"
paths:
/all:
get:
description: Retrieve all hosts from the database
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.Host'
type: array
summary: Get all hosts
tags:
- hosts
/config:
get:
description: Returns the current configuration used by the app
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Conf'
summary: Get application configuration
tags:
- system
/edit/{id}/{name}/{known}:
get:
description: Update a host's name and optionally toggle its "known" status
parameters:
- description: Host ID
in: path
name: id
required: true
type: string
- description: New name for the host
in: path
name: name
required: true
type: string
- description: Pass 'toggle' to flip the known/unknown status
in: path
name: known
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Edit host
tags:
- hosts
/history:
get:
description: Retrieve the complete history of all hosts. Not recommended, the
output can be a lot
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.Host'
type: array
summary: Get full history
tags:
- history
/history/{mac}:
get:
description: Retrieve the latest history entries for a specific host by MAC
address
parameters:
- description: MAC address of the host
in: path
name: mac
required: true
type: string
- description: Number of history entries to return
in: query
name: num
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.Host'
type: array
summary: Get history by MAC
tags:
- history
/history/{mac}/{date}:
get:
description: |-
Retrieve history for a specific host on a given date
The date format is flexible and can be:
- Year only: `2025`
- Year + month: `2025-09`
- Full date: `2025-09-06`
- Full timestamp: `2025-09-06 00:58:26`
parameters:
- description: MAC address of the host
in: path
name: mac
required: true
type: string
- description: Date filter (supports YYYY, YYYY-MM, YYYY-MM-DD, YYYY-MM-DD HH:mm:ss)
in: path
name: date
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.Host'
type: array
summary: Get history by date
tags:
- history
/host/{id}:
get:
description: Retrieve detailed information about a host by its unique ID
parameters:
- description: Host ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Host'
summary: Get host by ID
tags:
- hosts
/host/add/{mac}:
get:
description: |-
Add host by MAC, with optional Name, IP, Hardware
Returns `models.Host` with this MAC form DB, either just added or existing
parameters:
- description: Host MAC
in: path
name: mac
required: true
type: string
- description: Name
in: query
name: name
type: string
- description: IP
in: query
name: ip
type: string
- description: Hardware
in: query
name: hw
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Host'
summary: Add host manually
tags:
- hosts
/host/del/{id}:
get:
description: Remove a host from the database by its unique ID
parameters:
- description: Host ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Delete host
tags:
- hosts
/notify_test:
get:
description: Trigger a test notification to verify notification settings
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Send test notification
tags:
- system
/port/{addr}/{port}:
get:
description: Check whether a given TCP port on an address is open or closed
parameters:
- description: IP address or hostname
in: path
name: addr
required: true
type: string
- description: Port number
in: path
name: port
required: true
type: string
produces:
- application/json
responses:
"200":
description: true if open, false if closed
schema:
type: boolean
summary: Check port state
tags:
- network
/rescan:
get:
description: Manually trigger rescan
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Rescan all interfaces now
tags:
- system
/status/{iface}:
get:
description: Retrieve summary statistics of hosts, optionally filtered by interface
parameters:
- description: Interface name (omit for all interfaces)
in: path
name: iface
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Stat'
summary: Get network status
tags:
- system
/version:
get:
description: Returns the current running version of the application
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Get application version
tags:
- system
/wol/{mac}:
get:
description: Send a magic packet to wake up a host by its MAC address
parameters:
- description: MAC address of the host
in: path
name: mac
required: true
type: string
produces:
- application/json
responses:
"200":
description: true if sent successfully
schema:
type: boolean
summary: Send Wake-on-LAN packet
tags:
- network
swagger: "2.0"

View file

@ -1,19 +1,26 @@
module github.com/aceberg/WatchYourLAN
go 1.24.5
go 1.25.1
require (
github.com/aceberg/gorm-sqlite v0.0.0-20250723203925-9af3950b56af
github.com/aceberg/gorm-sqlite v1.6.0
github.com/gin-gonic/gin v1.10.1
github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/nicholas-fedor/shoutrrr v0.8.15
github.com/prometheus/client_golang v1.22.0
github.com/linde12/gowol v0.0.0-20180926075039-797e4d01634c
github.com/nicholas-fedor/shoutrrr v0.8.18
github.com/prometheus/client_golang v1.23.1
github.com/spf13/viper v1.20.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.1
gorm.io/gorm v1.30.3
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
@ -26,12 +33,17 @@ require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@ -39,9 +51,11 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -50,27 +64,29 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/oapi-codegen/runtime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/sagikazarmark/locafero v0.10.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.65.10 // indirect
modernc.org/mathutil v1.7.1 // indirect

View file

@ -1,6 +1,14 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/aceberg/gorm-sqlite v0.0.0-20250723203925-9af3950b56af h1:Y/PE5LSSwLmVw2juDC+U20agHCO5HdAsGE4Efpd0WVg=
github.com/aceberg/gorm-sqlite v0.0.0-20250723203925-9af3950b56af/go.mod h1:3g//0BOzaOv9dO8G9j7JZOJA6qIURpPaht9oDY7LKpA=
github.com/aceberg/gorm-sqlite v1.6.0 h1:I1c4uqC82Uc7JVt2+cjushdvQogN/C1I3XOiyn3yoUI=
github.com/aceberg/gorm-sqlite v1.6.0/go.mod h1:3g//0BOzaOv9dO8G9j7JZOJA6qIURpPaht9oDY7LKpA=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -16,6 +24,7 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -29,12 +38,24 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -45,17 +66,19 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
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/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
@ -68,12 +91,14 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
@ -83,14 +108,24 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linde12/gowol v0.0.0-20180926075039-797e4d01634c h1:QRJTb9zWXQL+yUajUqbp+VLtN+DQaYRloOxNwylsuVc=
github.com/linde12/gowol v0.0.0-20180926075039-797e4d01634c/go.mod h1:YeHfx3xIWda3noSterlj6d3+PdRRCTRox269+zhLmbM=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -104,40 +139,41 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.8.15 h1:kPFBGcuen8/FlcDqSZAMUi1ITbjXRW/vYThp5Pmpsi0=
github.com/nicholas-fedor/shoutrrr v0.8.15/go.mod h1:iCO6mfrMBrNMyMludLmj4EIgr+tx4tmwcZ5Hxjx1jwI=
github.com/nicholas-fedor/shoutrrr v0.8.18 h1:xYVtx2ipcEoLJFWViRpndC8zuTSRbhPEehsTkEBO4NQ=
github.com/nicholas-fedor/shoutrrr v0.8.18/go.mod h1:RYx646RI7DazdYQo49IWCuMeqq+D5zC3//F6I+qhCgY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/onsi/ginkgo/v2 v2.25.2 h1:hepmgwx1D+llZleKQDMEvy8vIlCxMGt7W5ZxDjIEhsw=
github.com/onsi/ginkgo/v2 v2.25.2/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/client_golang v1.23.1 h1:w6gXMLQGgd0jXXlote9lRHMe0nG01EbnJT+C0EJru2Y=
github.com/prometheus/client_golang v1.23.1/go.mod h1:br8j//v2eg2K5Vvna5klK8Ku5pcU5r4ll73v6ik5dIQ=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
@ -145,55 +181,99 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.30.3 h1:QiG8upl0Sg9ba2Zatfjy0fy4It2iNBL2/eMdvEkdXNs=
gorm.io/gorm v1.30.3/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=

View file

@ -0,0 +1,60 @@
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/gdb"
)
// getHistory godoc
// @Summary Get full history
// @Description Retrieve the complete history of all hosts. Not recommended, the output can be a lot
// @Tags history
// @Produce json
// @Success 200 {array} models.Host
// @Router /history [get]
func getHistory(c *gin.Context) {
hosts, _ := gdb.Select("history")
c.IndentedJSON(http.StatusOK, hosts)
}
// getHistoryByMAC godoc
// @Summary Get history by MAC
// @Description Retrieve the latest history entries for a specific host by MAC address
// @Tags history
// @Produce json
// @Param mac path string true "MAC address of the host"
// @Param num query int true "Number of history entries to return"
// @Success 200 {array} models.Host
// @Router /history/{mac} [get]
func getHistoryByMAC(c *gin.Context) {
mac := c.Param("mac")
numStr := c.Query("num")
num, _ := strconv.Atoi(numStr)
hosts := gdb.SelectLatest(mac, num)
c.IndentedJSON(http.StatusOK, hosts)
}
// getHistoryByDate godoc
// @Summary Get history by date
// @Description Retrieve history for a specific host on a given date
// @Description The date format is flexible and can be:
// @Description - Year only: `2025`
// @Description - Year + month: `2025-09`
// @Description - Full date: `2025-09-06`
// @Description - Full timestamp: `2025-09-06 00:58:26`
// @Tags history
// @Produce json
// @Param mac path string true "MAC address of the host"
// @Param date path string true "Date filter (supports YYYY, YYYY-MM, YYYY-MM-DD, YYYY-MM-DD HH:mm:ss)"
// @Success 200 {array} models.Host
// @Router /history/{mac}/{date} [get]
func getHistoryByDate(c *gin.Context) {
mac := c.Param("mac")
date := c.Param("date")
hosts := gdb.SelectByDate(mac, date)
c.IndentedJSON(http.StatusOK, hosts)
}

View file

@ -0,0 +1,120 @@
package api
import (
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/gdb"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// getAllHosts godoc
// @Summary Get all hosts
// @Description Retrieve all hosts from the database
// @Tags hosts
// @Produce json
// @Success 200 {array} models.Host
// @Router /all [get]
func getAllHosts(c *gin.Context) {
allHosts, _ := gdb.Select("now")
c.IndentedJSON(http.StatusOK, allHosts)
}
// getHost godoc
// @Summary Get host by ID
// @Description Retrieve detailed information about a host by its unique ID
// @Tags hosts
// @Produce json
// @Param id path string true "Host ID"
// @Success 200 {object} models.Host
// @Router /host/{id} [get]
func getHost(c *gin.Context) {
idStr := c.Param("id")
host := getHostByID(idStr) // functions.go
_, host.DNS = check.DNS(host)
c.IndentedJSON(http.StatusOK, host)
}
// delHost godoc
// @Summary Delete host
// @Description Remove a host from the database by its unique ID
// @Tags hosts
// @Produce json
// @Param id path string true "Host ID"
// @Success 200 {string} string "OK"
// @Router /host/del/{id} [get]
func delHost(c *gin.Context) {
idStr := c.Param("id")
host := getHostByID(idStr) // functions.go
gdb.Delete("now", host.ID)
slog.Info("Deleting from DB", "host", host)
c.IndentedJSON(http.StatusOK, "OK")
}
// addHost godoc
// @Summary Add host manually
// @Description Add host by MAC, with optional Name, IP, Hardware
// @Description Returns `models.Host` with this MAC form DB, either just added or existing
// @Tags hosts
// @Produce json
// @Param mac path string true "Host MAC"
// @Param name query string false "Name"
// @Param ip query string false "IP"
// @Param hw query string false "Hardware"
// @Success 200 {object} models.Host
// @Router /host/add/{mac} [get]
func addHost(c *gin.Context) {
mac := c.Param("mac")
hosts := gdb.SelectByMAC("now", mac)
if len(hosts) > 0 {
slog.Warn("Host with this MAC already exists", "host", hosts[0])
} else {
var host models.Host
host.Mac = mac
host.Name = c.Query("name")
host.IP = c.Query("ip")
host.Hw = c.Query("hw")
gdb.Update("now", host)
hosts = gdb.SelectByMAC("now", mac)
slog.Info("Added host to DB", "host", hosts[0])
}
c.IndentedJSON(http.StatusOK, hosts[0])
}
// editHost godoc
// @Summary Edit host
// @Description Update a host's name and optionally toggle its "known" status
// @Tags hosts
// @Produce json
// @Param id path string true "Host ID"
// @Param name path string true "New name for the host"
// @Param known path string false "Pass 'toggle' to flip the known/unknown status"
// @Success 200 {string} string "OK"
// @Router /edit/{id}/{name}/{known} [get]
func editHost(c *gin.Context) {
idStr := c.Param("id")
name := c.Param("name")
toggleKnown := c.Param("known")
host := getHostByID(idStr) // functions.go
host.Name = name
if toggleKnown == "/toggle" {
host.Known = 1 - host.Known
}
gdb.Update("now", host)
c.IndentedJSON(http.StatusOK, "OK")
}

View file

@ -0,0 +1,51 @@
package api
import (
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
"github.com/linde12/gowol"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/portscan"
)
// getPortState godoc
// @Summary Check port state
// @Description Check whether a given TCP port on an address is open or closed
// @Tags network
// @Produce json
// @Param addr path string true "IP address or hostname"
// @Param port path string true "Port number"
// @Success 200 {boolean} bool "true if open, false if closed"
// @Router /port/{addr}/{port} [get]
func getPortState(c *gin.Context) {
addr := c.Param("addr")
port := c.Param("port")
state := portscan.IsOpen(addr, port)
c.IndentedJSON(http.StatusOK, state)
}
// sendWOL godoc
// @Summary Send Wake-on-LAN packet
// @Description Send a magic packet to wake up a host by its MAC address
// @Tags network
// @Produce json
// @Param mac path string true "MAC address of the host"
// @Success 200 {boolean} bool "true if sent successfully"
// @Router /wol/{mac} [get]
func sendWOL(c *gin.Context) {
mac := c.Param("mac")
packet, err := gowol.NewMagicPacket(mac)
if !check.IfError(err) {
err = packet.Send("255.255.255.255")
slog.Info("Wake-on-LAN: " + mac)
}
c.IndentedJSON(http.StatusOK, !check.IfError(err))
}

View file

@ -0,0 +1,104 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/gdb"
"github.com/aceberg/WatchYourLAN/internal/models"
"github.com/aceberg/WatchYourLAN/internal/notify"
"github.com/aceberg/WatchYourLAN/internal/routines"
)
// getVersion godoc
// @Summary Get application version
// @Description Returns the current running version of the application
// @Tags system
// @Produce json
// @Success 200 {string} string
// @Router /version [get]
func getVersion(c *gin.Context) {
c.IndentedJSON(http.StatusOK, conf.AppConfig.Version)
}
// triggerRescan godoc
// @Summary Rescan all interfaces now
// @Description Manually trigger rescan
// @Tags system
// @Produce json
// @Success 200 {string} string "OK"
// @Router /rescan [get]
func triggerRescan(c *gin.Context) {
routines.ScanRestart()
c.Status(http.StatusOK)
}
// getConfig godoc
// @Summary Get application configuration
// @Description Returns the current configuration used by the app
// @Tags system
// @Produce json
// @Success 200 {object} models.Conf
// @Router /config [get]
func getConfig(c *gin.Context) {
c.IndentedJSON(http.StatusOK, conf.AppConfig)
}
// notifyTest godoc
// @Summary Send test notification
// @Description Trigger a test notification to verify notification settings
// @Tags system
// @Produce json
// @Success 200 {string} string "OK"
// @Router /notify_test [get]
func notifyTest(c *gin.Context) {
notify.Test()
c.Status(http.StatusOK)
}
// getStatus godoc
// @Summary Get network status
// @Description Retrieve summary statistics of hosts, optionally filtered by interface
// @Tags system
// @Produce json
// @Param iface path string false "Interface name (omit for all interfaces)"
// @Success 200 {object} models.Stat
// @Router /status/{iface} [get]
func getStatus(c *gin.Context) {
var status models.Stat
var searchHosts []models.Host
allHosts, _ := gdb.Select("now")
iface := c.Param("iface")
iface = iface[1:]
if iface != "" && iface != "undefined" {
for _, host := range allHosts {
if iface == host.Iface {
searchHosts = append(searchHosts, host)
}
}
} else {
searchHosts = allHosts
}
for _, host := range searchHosts {
status.Total = status.Total + 1
if host.Known > 0 {
status.Known = status.Known + 1
} else {
status.Unknown = status.Unknown + 1
}
if host.Now > 0 {
status.Online = status.Online + 1
} else {
status.Offline = status.Offline + 1
}
}
c.IndentedJSON(http.StatusOK, status)
}

View file

@ -0,0 +1,41 @@
package api
import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// Routes - start API routes
func Routes(router *gin.Engine) {
r0 := router.Group("/api")
{
r0.GET("/all", getAllHosts) // api-hosts.go
r0.GET("/edit/:id/:name/*known", editHost) // api-hosts.go
r0.GET("/host/:id", getHost) // api-hosts.go
r0.GET("/host/del/:id", delHost) // api-hosts.go
r0.GET("/host/add/:mac", addHost) // api-hosts.go
r0.GET("/config", getConfig) // api-system.go
r0.GET("/notify_test", notifyTest) // api-system.go
r0.GET("/status/*iface", getStatus) // api-system.go
r0.GET("/version", getVersion) // api-system.go
r0.GET("/rescan", triggerRescan) // api-system.go
r0.GET("/history", getHistory) // api-history.go
r0.GET("/history/:mac", getHistoryByMAC) // api-history.go
r0.GET("/history/:mac/:date", getHistoryByDate) // api-history.go
r0.GET("/port/:addr/:port", getPortState) // api-network.go
r0.GET("/wol/:mac", sendWOL) // api-network.go
r0.POST("/config/", saveConfigHandler) // config.go
r0.POST("/config_settings/", saveSettingsHandler) // config.go
r0.POST("/config_influx/", saveInfluxHandler) // config.go
r0.POST("/config_prometheus/", savePrometheusHandler) // config.go
}
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

View file

@ -24,9 +24,9 @@ func SelectByID(id int) (host models.Host) {
}
// SelectByMAC - get all hosts by MAC
func SelectByMAC(mac string) (hosts []models.Host) {
func SelectByMAC(table, mac string) (hosts []models.Host) {
tab := db.Table("history")
tab := db.Table(table)
tab.Where("\"MAC\" = ?", mac).Find(&hosts)
return hosts

View file

@ -17,7 +17,6 @@ func Unknown(host models.Host) {
msg := fmt.Sprintf("Unknown host found. Name: '%s', IP: '%s', MAC: '%s', Hw: '%s', Iface: '%s'", host.DNS, host.IP, host.Mac, host.Hw, host.Iface)
slog.Warn(msg)
shout(msg)
}
@ -25,6 +24,7 @@ func Unknown(host models.Host) {
func Test() {
msg := "test notification"
slog.Info("Sending " + msg)
shout(msg)
}

View file

@ -13,13 +13,12 @@ func IsOpen(host, port string) bool {
target := fmt.Sprintf("%s:%s", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return false
}
if conn != nil {
conn.Close()
return true
if err == nil {
err = conn.Close()
if err == nil {
return true
}
}
return false

File diff suppressed because one or more lines are too long

View file

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

View file

@ -1,4 +1,4 @@
import{B as y,C as f,D as v,o as m,E as H,e as u,S as d,t as D,b as I,s as b,G as h,l as M,F as g}from"./index.js";async function w(a,r){let e=[];return r===""?e=await y(a):e=await f(a,r),e!=null?(e.sort((s,t)=>s.Date<t.Date?1:-1),e):[]}var x=D("<i>");function F(a){const[r,e]=v([]);let s;return m(async()=>{const t=await w(a.mac,a.date);e(t),s=setInterval(async()=>{const o=await w(a.mac,a.date);e(o)},6e4)}),H(()=>{clearInterval(s)}),u(g,{each:r,children:(t,o)=>u(d,{get when(){return o()<M()},get children(){var i=x();return I(n=>{var c="Date:"+t.Date+`
import{C as y,D as f,E as v,o as m,G as H,e as u,S as d,t as D,b as I,s as b,H as h,l as M,F as g}from"./index.js";async function w(a,r){let e=[];return r===""?e=await y(a):e=await f(a,r),e!=null?(e.sort((s,t)=>s.Date<t.Date?1:-1),e):[]}var x=D("<i>");function F(a){const[r,e]=v([]);let s;return m(async()=>{const t=await w(a.mac,a.date);e(t),s=setInterval(async()=>{const o=await w(a.mac,a.date);e(o)},6e4)}),H(()=>{clearInterval(s)}),u(g,{each:r,children:(t,o)=>u(d,{get when(){return o()<M()},get children(){var i=x();return I(n=>{var c="Date:"+t.Date+`
Iface:`+t.Iface+`
IP:`+t.IP+`
Known:`+t.Known,l=t.Now===0?"my-box-off":"my-box-on";return c!==n.e&&b(i,"title",n.e=c),l!==n.t&&h(i,n.t=l),n},{e:void 0,t:void 0}),i}})})}export{F as M};

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1 @@
VERSION=2.1.4

View file

@ -3,11 +3,11 @@ USR_NAME=aceberg
build:
npm run build && \
rm ../internal/web/public/assets/* && \
cp -r dist/assets ../internal/web/public
rm ../backend/internal/web/public/assets/* && \
cp -r dist/assets ../backend/internal/web/public
replace:
cd ../internal/web/public/assets/ && \
cd ../backend/internal/web/public/assets/ && \
cat index.js | sed 's/assets/fs\/public\/assets/g;s/http:\/\/0.0.0.0:8840//' > tmp && \
mv tmp index.js

View file

@ -1,13 +1,14 @@
{
"name": "watchyourlan",
"version": "0.0.0",
"version": "2.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "watchyourlan",
"version": "0.0.0",
"version": "2.1.3",
"dependencies": {
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5"
},
@ -964,6 +965,15 @@
"win32"
]
},
"node_modules/@solid-primitives/scheduled": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/scheduled/-/scheduled-1.5.2.tgz",
"integrity": "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==",
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solidjs/router": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz",

View file

@ -1,7 +1,7 @@
{
"name": "watchyourlan",
"private": true,
"version": "2.1.3",
"version": "2.1.4",
"type": "module",
"scripts": {
"dev": "vite",
@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5"
},

View file

@ -1,8 +1,9 @@
import { Show } from "solid-js";
import { editNames, setEditNames } from "../../functions/exports";
import { editNames, selectedIDs, setEditNames } from "../../functions/exports";
import Filter from "../Filter";
import Search from "../Search";
import { getHosts } from "../../functions/atstart";
import { apiDelHost } from "../../functions/api";
function CardHead() {
@ -13,6 +14,16 @@ function CardHead() {
setEditNames(toggle);
};
const handleDel = async () => {
const ids = selectedIDs();
for (let id of ids) {
await apiDelHost(id);
}
window.location.href = '/';
};
return (
<div class="row">
<div class="col-md mt-1 mb-1">
@ -25,9 +36,10 @@ function CardHead() {
<Search></Search>
<Show
when={editNames()}
fallback={<button class="btn btn-outline-primary" title="Click to edit names" onClick={[handleEditNames, true]}>Edit names</button>}
fallback={<button class="btn btn-outline-primary" title="Toggle edit" onClick={[handleEditNames, true]}>Edit</button>}
>
<button class="btn btn-primary" onClick={[handleEditNames, false]}>Edit names</button>
<button type="button" onClick={handleDel} title="Delete selected hosts" class="btn btn-outline-danger">Delete selected</button>
<button class="btn btn-primary" title="Toggle edit" onClick={[handleEditNames, false]}>Edit</button>
</Show>
</div>
</div>

View file

@ -36,7 +36,7 @@ function TableHead() {
title={"Sort by " + key}
></i></th>
}</For>
<th style="width: 2em;"></th>
<th style="width: 2em;" title="Edit"><i class="bi bi-pencil-fill"></i></th>
</tr>
</thead>
)

View file

@ -1,7 +1,9 @@
import { createSignal, Show } from "solid-js";
import { editNames } from "../../functions/exports";
import { editNames, selectedIDs, setSelectedIDs } from "../../functions/exports";
import { apiEditHost } from "../../functions/api";
import { debounce } from "@solid-primitives/scheduled";
function TableRow(_props: any) {
const [name, setName] = createSignal(_props.host.Name);
@ -14,12 +16,27 @@ function TableRow(_props: any) {
let known:boolean;
_props.host.Known === 1 ? known = true : known = false;
const debouncedApi = debounce(async (val: string) => {
await apiEditHost(_props.host.ID, val, "");
}, 300);
const handleInput = async (n: string) => {
setName(n);
await apiEditHost(_props.host.ID, name(), '');
debouncedApi(n);
};
const handleToggle = async () => {
await apiEditHost(_props.host.ID, name(), 'toggle');
await apiEditHost(_props.host.ID, name(), "toggle");
};
const handleCheck = (checked: boolean) => {
const id = _props.host.ID;
setSelectedIDs(prev => {
if (checked) {
return prev.includes(id) ? prev : [...prev, id];
} else {
return prev.filter(item => item !== id);
}
});
};
return (
@ -47,9 +64,20 @@ function TableRow(_props: any) {
</td>
<td>{now}</td>
<td>
<a href={"/host/" + _props.host.ID}>
<i class="bi bi-three-dots-vertical my-btn p-2" title="More"></i>
</a>
<Show
when={editNames()}
fallback={
<a href={"/host/" + _props.host.ID}>
<i class="bi bi-three-dots-vertical my-btn p-2" title="More"></i>
</a>}
>
<input
type="checkbox"
class="form-check-input"
checked={selectedIDs().includes(_props.host.ID)}
onChange={e => handleCheck((e.target as HTMLInputElement).checked)}
/>
</Show>
</td>
</tr>
)

View file

@ -17,17 +17,45 @@ function About() {
<div class="card-header">
About (<a href={link()} target="_blank">{version()}</a>)
</div>
<div class="card-body">
<p> After changing <b>Host</b> or <b>Port</b> the app must be restarted</p>
<p> <b>Shoutrrr URL</b> provides notifications to Discord, Email, Gotify, Telegram and other services. <a href="https://nicholas-fedor.github.io/shoutrrr/" target="_blank">Link to documentation</a></p>
<p> <b>Interfaces</b> - one or more, space separated</p>
<p> <b>Timeout (seconds)</b> - time between scans</p>
<p> <b>Args for arp-scan</b> - pass your own arguments to <code>arp-scan</code>. Enable <b>debug</b> log level to see resulting command. (Example: <code>-r 1</code>). See <a href="https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md" target="_blank">docs</a> for more.</p>
<p> <b>Arp Strings</b> - can setup scans for <code>vlans</code>, <code>docker0</code> and etcetera. See <a href="https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md" target="_blank">docs</a> for more.</p>
<p> <b>Trim History</b> - remove history after (hours)</p>
<p> <b>Store History in DB</b> - DEPRECATED. Now History is always stored in DB. Use <b>Trim History</b> to reduce DB size</p>
<p> <b>PG Connect URL</b> - address to connect to PostgreSQL DB. (Example: <code>postgres://username:password@192.168.0.1:5432/dbname?sslmode=disable</code>). Full list of URL parameters <a href="https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters" target="_blank">here</a></p>
<p> If you find this app useful, please, <a href="https://github.com/aceberg#donate" target="_blank">donate</a></p>
<div class="card-body table-responsive">
<table class="table table-striped"><tbody>
<tr>
<td><b>Swagger API docs</b></td>
<td><a href="/swagger/index.html" target="_blank">/swagger/index.html</a></td>
</tr>
<tr>
<td><b>Local node-bootstrap URL</b></td>
<td>local themes and fonts (optional). If empty, the app will pull everything from <code>cdn</code></td>
</tr>
<tr>
<td><b>Shoutrrr URL</b></td>
<td>provides notifications to Discord, Email, Gotify, Telegram and other services. <a href="https://shoutrrr.nickfedor.com/services/overview/" target="_blank">Link to documentation</a></td>
</tr>
<tr>
<td><b>Interfaces</b></td>
<td>one or more, space separated</td>
</tr>
<tr>
<td><b>Timeout (seconds)</b></td>
<td>time between scans</td>
</tr>
<tr>
<td><b>Args for arp-scan</b></td>
<td>pass your own arguments to <code>arp-scan</code>. Enable <b>debug</b> log level to see resulting command. (Example: <code>-r 1</code>). See <a href="https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md" target="_blank">docs</a> for more</td>
</tr>
<tr>
<td><b>Arp Strings</b></td>
<td>can setup scans for <code>vlans</code>, <code>docker0</code> and etcetera. See <a href="https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md" target="_blank">docs</a> for more</td>
</tr>
<tr>
<td><b>Trim History</b></td>
<td>remove history after (hours)</td>
</tr>
<tr>
<td><b>PG Connect URL</b></td>
<td>address to connect to PostgreSQL DB. (Example: <code>postgres://username:password@192.168.0.1:5432/dbname?sslmode=disable</code>). Full list of URL parameters <a href="https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters" target="_blank">here</a></td>
</tr>
</tbody></table>
</div>
</div>
)

View file

@ -0,0 +1,17 @@
function Donate() {
return (
<div class="card border-info">
<div class="card-header">Donations and Work</div>
<div class="card-body">
<p>Every <a href="https://github.com/aceberg#donate" target="_blank">donation</a> is truly appreciated!</p>
<p>I am offering self-hosted and mobile apps development for a small price.
The Open Source apps I created are listed in my <a href="https://aceberg.github.io/portfolio/" target="_blank">portfolio</a>. They are all human-coded, not generated by AI. Please, <a href="https://github.com/aceberg#contact" target="_blank">contact</a> me, if you are interested.</p>
</div>
</div>
)
}
export default Donate

View file

@ -71,7 +71,7 @@ function Scan() {
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
<td></td>
<td class="text-muted">*Pressing <b>Save</b> button will trigger rescan</td>
</tr>
</tbody></table>
</form>

View file

@ -1,15 +1,19 @@
import { apiDelHost, apiEditHost } from "../../functions/api";
import { getHosts } from "../../functions/atstart";
import { apiDelHost, apiEditHost, apiWOL } from "../../functions/api";
import { debounce } from "@solid-primitives/scheduled";
function HostCard(_props: any) {
let name:string = "";
const debouncedApi = debounce(async (val: string) => {
await apiEditHost(_props.host.ID, val, "");
}, 300);
const handleInput = async (n: string) => {
name = n;
await apiEditHost(_props.host.ID, name, '');
getHosts();
debouncedApi(n);
};
const handleToggle = async () => {
@ -19,7 +23,6 @@ function HostCard(_props: any) {
}
await apiEditHost(_props.host.ID, name, 'toggle');
getHosts();
};
const handleDel = async () => {
@ -28,6 +31,11 @@ function HostCard(_props: any) {
window.location.href = '/';
};
const handleWOL = async () => {
await apiWOL(_props.host.Mac);
};
return (
<div class="card border-primary">
<div class="card-header">Host</div>
@ -90,7 +98,10 @@ function HostCard(_props: any) {
<td>{_props.host.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>
}
&nbsp;&nbsp;&nbsp;
<button type="button" onClick={handleWOL} class="btn btn-outline-success">Wake-on-LAN</button>
</td>
</tr>
</tbody>
</table>

View file

@ -73,4 +73,12 @@ export const apiGetHistoryByDate = async (mac:string, date: string) => {
const hosts = await (await fetch(url)).json();
return hosts;
};
export const apiWOL = async (mac:string) => {
const url = apiPath+'/api/wol/'+mac;
const res = await (await fetch(url)).json();
return res;
};

View file

@ -90,4 +90,6 @@ export const [editNames, setEditNames] = createSignal(false);
export const [show, setShow] = createSignal<number>(200);
export const [histUpdOnFilter, setHistUpdOnFilter] = createSignal(false);
export const [histUpdOnFilter, setHistUpdOnFilter] = createSignal(false);
export const [selectedIDs, setSelectedIDs] = createSignal<number[]>([]);

View file

@ -1,13 +1,18 @@
import { For } from "solid-js";
import { For, onMount } 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";
import { getHosts } from "../functions/atstart";
function Body() {
onMount(() => {
getHosts();
});
return (
<div class="card border-primary">
<div class="card-header">

View file

@ -1,5 +1,6 @@
import About from "../components/Config/About"
import Basic from "../components/Config/Basic"
import Donate from "../components/Config/Donate"
import Influx from "../components/Config/Influx"
import Prometheus from "../components/Config/Prometheus"
import Scan from "../components/Config/Scan"
@ -9,13 +10,20 @@ function Config() {
return (
<div class="row">
<div class="col-md">
<Basic></Basic>
<div class="mt-4">
<Donate></Donate>
</div>
<div class="mt-4 mb-4">
<Scan></Scan>
</div>
</div>
<div class="col-md">
<Influx></Influx>
<div class="mt-4">
<Prometheus></Prometheus>
</div>

View file

@ -1,154 +0,0 @@
package api
import (
"log/slog"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/gdb"
"github.com/aceberg/WatchYourLAN/internal/models"
"github.com/aceberg/WatchYourLAN/internal/notify"
"github.com/aceberg/WatchYourLAN/internal/portscan"
)
func getAllHosts(c *gin.Context) {
allHosts, _ := gdb.Select("now")
c.IndentedJSON(http.StatusOK, allHosts)
}
func getVersion(c *gin.Context) {
c.IndentedJSON(http.StatusOK, conf.AppConfig.Version)
}
func getConfig(c *gin.Context) {
c.IndentedJSON(http.StatusOK, conf.AppConfig)
}
func getHistory(c *gin.Context) {
hosts, _ := gdb.Select("history")
c.IndentedJSON(http.StatusOK, hosts)
}
func getHistoryByMAC(c *gin.Context) {
mac := c.Param("mac")
numStr := c.Query("num")
num, _ := strconv.Atoi(numStr)
hosts := gdb.SelectLatest(mac, num)
c.IndentedJSON(http.StatusOK, hosts)
}
func getHistoryByDate(c *gin.Context) {
mac := c.Param("mac")
date := c.Param("date")
hosts := gdb.SelectByDate(mac, date)
c.IndentedJSON(http.StatusOK, hosts)
}
func getHost(c *gin.Context) {
idStr := c.Param("id")
host := getHostByID(idStr) // functions.go
_, host.DNS = check.DNS(host)
c.IndentedJSON(http.StatusOK, host)
}
func delHost(c *gin.Context) {
idStr := c.Param("id")
host := getHostByID(idStr) // functions.go
gdb.Delete("now", host.ID)
slog.Info("Deleting from DB", "host", host)
c.IndentedJSON(http.StatusOK, "OK")
}
func getPortState(c *gin.Context) {
addr := c.Param("addr")
port := c.Param("port")
state := portscan.IsOpen(addr, port)
c.IndentedJSON(http.StatusOK, state)
}
func editHost(c *gin.Context) {
idStr := c.Param("id")
name := c.Param("name")
toggleKnown := c.Param("known")
host := getHostByID(idStr) // functions.go
host.Name = name
if toggleKnown == "/toggle" {
host.Known = 1 - host.Known
}
gdb.Update("now", host)
c.IndentedJSON(http.StatusOK, "OK")
}
func notifyTest(c *gin.Context) {
notify.Test()
c.Status(http.StatusOK)
}
func getStatus(c *gin.Context) {
var status models.Stat
var searchHosts []models.Host
allHosts, _ := gdb.Select("now")
iface := c.Param("iface")
iface = iface[1:]
if iface != "" {
for _, host := range allHosts {
if iface == host.Iface {
searchHosts = append(searchHosts, host)
}
}
} else {
searchHosts = allHosts
}
for _, host := range searchHosts {
status.Total = status.Total + 1
if host.Known > 0 {
status.Known = status.Known + 1
} else {
status.Unknown = status.Unknown + 1
}
if host.Now > 0 {
status.Online = status.Online + 1
} else {
status.Offline = status.Offline + 1
}
}
c.IndentedJSON(http.StatusOK, status)
}

View file

@ -1,27 +0,0 @@
package api
import (
"github.com/gin-gonic/gin"
)
// Routes - start API routes
func Routes(router *gin.Engine) {
router.GET("/api/all", getAllHosts) // api.go
router.GET("/api/config", getConfig) // api.go
router.GET("/api/edit/:id/:name/*known", editHost) // api.go
router.GET("/api/history", getHistory) // api.go
router.GET("/api/history/:mac", getHistoryByMAC) // api.go
router.GET("/api/history/:mac/:date", getHistoryByDate) // api.go
router.GET("/api/host/:id", getHost) // api.go
router.GET("/api/host/del/:id", delHost) // api.go
router.GET("/api/notify_test", notifyTest) // api.go
router.GET("/api/port/:addr/:port", getPortState) // api.go
router.GET("/api/status/*iface", getStatus) // api.go
router.GET("/api/version", getVersion) // api.go
router.POST("/api/config/", saveConfigHandler) // config.go
router.POST("/api/config_settings/", saveSettingsHandler) // config.go
router.POST("/api/config_influx/", saveInfluxHandler) // config.go
router.POST("/api/config_prometheus/", savePrometheusHandler) // config.go
}

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
VERSION=2.1.3