Compare commits
169 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca9f644bcc | ||
|
|
847b4caaf8 | ||
|
|
24ee0acb45 | ||
|
|
1271b7b42e | ||
|
|
92e9a80770 | ||
|
|
c615198560 | ||
|
|
f1bb8166ba | ||
|
|
dee79fc74f | ||
|
|
6781adb430 | ||
|
|
2544a3f390 | ||
|
|
d314297fa2 | ||
|
|
f6977e290b | ||
|
|
48aba334a0 | ||
|
|
4d820890bb | ||
|
|
595f04540b | ||
|
|
e6f3b150ab | ||
|
|
5c449f76ba | ||
|
|
1f924034b0 | ||
|
|
6b2851264f | ||
|
|
24afe6c96f | ||
|
|
c180ea2a6a | ||
|
|
c23be08cca | ||
|
|
b51161bb3e | ||
|
|
2957a51613 | ||
|
|
7743c41f10 | ||
|
|
58453a7d8f | ||
|
|
f68cd0ddd6 | ||
|
|
f161140e94 | ||
|
|
47d5881b56 | ||
|
|
74c26d08a8 | ||
|
|
131efe7f36 | ||
|
|
bf075aabc2 | ||
|
|
9b78b7115f | ||
|
|
ce570b46bd | ||
|
|
5055cd3d00 | ||
|
|
0cd7cc2730 | ||
|
|
5d2e77dd42 | ||
|
|
022f266038 | ||
|
|
ffc2d2fdbe | ||
|
|
113cf7cb18 | ||
|
|
8f122db640 | ||
|
|
70f2a9aeee | ||
|
|
545eac368e | ||
|
|
a5663ec7d4 | ||
|
|
d17b324e6e | ||
|
|
b31107bc42 | ||
|
|
6e57809d97 | ||
|
|
745dd0c2b1 | ||
|
|
93852060cc | ||
|
|
856c2fc514 | ||
|
|
a9972ac0ce | ||
|
|
37bd7ca198 | ||
|
|
e8428677a4 | ||
|
|
3e416925da | ||
|
|
a2fd352bf1 | ||
|
|
0b34f6181f | ||
|
|
92f89bb313 | ||
|
|
07637a7c94 | ||
|
|
fb1122c03d | ||
|
|
4fe2e36c77 | ||
|
|
eda622b6e3 | ||
|
|
a3d86cb7d7 | ||
|
|
9c7809a513 | ||
|
|
04c445cf77 | ||
|
|
16f30a3159 | ||
|
|
0da9fcfd97 | ||
|
|
eb96dabb06 | ||
|
|
a4f88303d1 | ||
|
|
638e57b107 | ||
|
|
6ab27978f0 | ||
|
|
f7b9f87a09 | ||
|
|
1e1f337ecd | ||
|
|
9132401ce0 | ||
|
|
585ee35c59 | ||
|
|
6e2511bee7 | ||
|
|
53dcf3cc38 | ||
|
|
d26ff73581 | ||
|
|
67bfbe67a4 | ||
|
|
46ed12b4af | ||
|
|
3fccc2f149 | ||
|
|
2934878b5a | ||
|
|
ee850d2576 | ||
|
|
b646fb74e3 | ||
|
|
a76eb3ffa2 | ||
|
|
b37d5c2a9b | ||
|
|
30532a0e32 | ||
|
|
1eaf237ba6 | ||
|
|
ada1b98598 | ||
|
|
0d0e209661 | ||
|
|
cd6d521ed1 | ||
|
|
ebbc899f8e | ||
|
|
936d4aa312 | ||
|
|
246b35dbaf | ||
|
|
93dec3961a | ||
|
|
180e46a934 | ||
|
|
78772e9238 | ||
|
|
74b1b3cb2e | ||
|
|
2253ebc25d | ||
|
|
f756f770eb | ||
|
|
3d5b86cf4f | ||
|
|
795a46fd3c | ||
|
|
c1d4f7c60a | ||
|
|
4fbe384c80 | ||
|
|
01323c6c65 | ||
|
|
70581a6271 | ||
|
|
78641c8a91 | ||
|
|
7b45306415 | ||
|
|
03846c4365 | ||
|
|
974a5acb9b | ||
|
|
19610b07c7 | ||
|
|
61dce18880 | ||
|
|
62302d19fa | ||
|
|
3aa63fef82 | ||
|
|
6b77b66d4e | ||
|
|
316a02fa2f | ||
|
|
0187d9b1bb | ||
|
|
eaa0c27bd7 | ||
|
|
d6ee3a059f | ||
|
|
4ddb03719f | ||
|
|
1ab69ba1ea | ||
|
|
9aa39c471e | ||
|
|
95e2f2a4de | ||
|
|
8542417d40 | ||
|
|
17b6545eaa | ||
|
|
90e4c07a37 | ||
|
|
6f703c51be | ||
|
|
b86ee2ba73 | ||
|
|
0d10123d12 | ||
|
|
0000b54fb8 | ||
|
|
54513cddd9 | ||
|
|
409274759f | ||
|
|
f7f301b6ca | ||
|
|
b0a326c1e2 | ||
|
|
534c7cedb0 | ||
|
|
3a3bc5da09 | ||
|
|
73db704cf7 | ||
|
|
e6a5bf852b | ||
|
|
26f3759936 | ||
|
|
0c410d9458 | ||
|
|
8826bd8cb6 | ||
|
|
5e0e42a7a2 | ||
|
|
d765d304f6 | ||
|
|
d78937852e | ||
|
|
a7f8dd58f2 | ||
|
|
eee3c1d549 | ||
|
|
d21f6599e4 | ||
|
|
8e535c086e | ||
|
|
e8475c1eac | ||
|
|
3525cf1950 | ||
|
|
de19da02b0 | ||
|
|
81a240c428 | ||
|
|
564ac35aa3 | ||
|
|
184440d5a7 | ||
|
|
64a4b17a8f | ||
|
|
0acd987313 | ||
|
|
946d21edbb | ||
|
|
27f4789d86 | ||
|
|
ac73c2e4de | ||
|
|
4ebd2271bf | ||
|
|
3403857442 | ||
|
|
c11e7a6956 | ||
|
|
a4a0fb2b48 | ||
|
|
a180ae3b92 | ||
|
|
b8f7d3bfb3 | ||
|
|
a0173f9132 | ||
|
|
b53e440e0e | ||
|
|
fde06e7a47 | ||
|
|
44b51d38d0 | ||
|
|
0ee8f9b6a0 |
2
.github/FUNDING.yml
vendored
|
|
@ -1,3 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
custom: ['https://boosty.to/aceberg/donate', 'https://aceberg.github.io/MyDocs/sponsor']
|
||||
custom: ['https://boosty.to/aceberg/donate', 'https://github.com/aceberg#donate']
|
||||
|
|
|
|||
28
.github/workflows/binary-release.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
name: Binary-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
name: Create release-artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- run: go version
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
workdir: backend
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
22
.github/workflows/dev-docker-io.yml
vendored
|
|
@ -14,13 +14,29 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Docker Image to docker.io
|
||||
uses: mr-smithers-excellent/docker-build-push@v5
|
||||
uses: mr-smithers-excellent/docker-build-push@v6
|
||||
with:
|
||||
image: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ env.TAGS }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Login to GHCR
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Build and push
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# context: .
|
||||
# platforms: linux/amd64
|
||||
# push: true
|
||||
# tags: |
|
||||
# ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.TAGS }}
|
||||
22
.github/workflows/main-docker-all.yml
vendored
|
|
@ -13,45 +13,49 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version tag from env file
|
||||
uses: c-py/action-dotenv-to-setenv@v2
|
||||
uses: c-py/action-dotenv-to-setenv@v5
|
||||
with:
|
||||
env-file: .version
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:v2
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:v2
|
||||
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
||||
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
|||
52
.github/workflows/new-dev-docker.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: New-Dev-Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches: [ "main" ]
|
||||
# paths:
|
||||
# - 'Dockerfile'
|
||||
# - 'src/**'
|
||||
|
||||
env:
|
||||
IMAGE_NAME: watchyourlan
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev
|
||||
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name: DockerHub-README
|
||||
name: README-to-DockerHub
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Sync README.md to DockerHub
|
||||
uses: ms-jpq/sync-dockerhub-readme@v1
|
||||
32
.github/workflows/release.yml
vendored
|
|
@ -1,32 +0,0 @@
|
|||
name: Binary-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
name: Create release-artifacts
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Get version tag from env file
|
||||
uses: c-py/action-dotenv-to-setenv@v2
|
||||
with:
|
||||
env-file: .version
|
||||
|
||||
- name: Build and archive
|
||||
run: build/ci/build.sh ${{ env.VERSION }}
|
||||
|
||||
- name: Build .deb package
|
||||
run: build/ci/build-deb.sh ${{ env.VERSION }}
|
||||
|
||||
- name: Upload the artifacts
|
||||
uses: skx/github-action-publish-binaries@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: 'watchyourlan-*'
|
||||
2
.version
|
|
@ -1 +1 @@
|
|||
internal/web/templates/version
|
||||
backend/internal/web/public/version
|
||||
150
CHANGELOG.md
|
|
@ -1,77 +1,91 @@
|
|||
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.0.1] - 2023-08-18
|
||||
## [v2.1.4] - 2025-09-10
|
||||
### Added
|
||||
- User Authentification
|
||||
- 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)
|
||||
|
||||
## [v1.0.0] - 2023-08-03
|
||||
### Changed
|
||||
- Migrated to YAML config file format
|
||||
|
||||
## [v0.9.4] - 2023-06-26
|
||||
### Added
|
||||
- Local node modules option
|
||||
|
||||
## [v0.9.3] - 2023-06-04
|
||||
### Added
|
||||
- Background color: light or dark
|
||||
|
||||
## [v0.9.2] - 2023-05-05
|
||||
### Added
|
||||
- Choose log level in config
|
||||
|
||||
### Changed
|
||||
- Move to go-1.20
|
||||
|
||||
## [v0.9.1] - 2023-01-16
|
||||
### Added
|
||||
- Scan host for open ports
|
||||
- App version on config page
|
||||
- Check if update is available
|
||||
- Button to test notifications
|
||||
|
||||
### Changed
|
||||
- Small fixes
|
||||
|
||||
## [v0.8.3] - 2023-01-03
|
||||
### Added
|
||||
- Clear table button (Config page)
|
||||
- Delete host button
|
||||
- Host name from DNS (Click on MAC to see host page)
|
||||
- Sort buttons for all fields
|
||||
|
||||
|
||||
## [v0.8.2] - 2023-01-02
|
||||
### Added
|
||||
- Option to ignore IP change [Issue #25](https://github.com/aceberg/WatchYourLAN/issues/25)
|
||||
|
||||
|
||||
## [v0.8.1] - 2022-12-29
|
||||
### Changed
|
||||
- Full code refactoring
|
||||
|
||||
### Added
|
||||
- Embed templates
|
||||
- GUI config page
|
||||
- Binary and Dep package
|
||||
|
||||
## [v0.7.5] - 2022-09-14
|
||||
## [v2.1.3] - 2025-07-26
|
||||
### Fixed
|
||||
- Sort by IP [Issue #4](https://github.com/aceberg/WatchYourLAN/issues/4), [Issue #14](https://github.com/aceberg/WatchYourLAN/issues/14)
|
||||
- Memory leak bug [#149](https://github.com/aceberg/WatchYourLAN/issues/149)
|
||||
- Duplicated devices bug [#187](https://github.com/aceberg/WatchYourLAN/issues/187) [#198](https://github.com/aceberg/WatchYourLAN/issues/198)
|
||||
|
||||
## [v0.7.3] - 2022-08-30
|
||||
### Added
|
||||
- Workflow to build ARM images
|
||||
### Changed
|
||||
- **DEPRECATED:** `HIST_IN_DB` config option. Now history is always stored in `DB`
|
||||
- Upd to `go 1.24.5`
|
||||
- Moved `DB` handling to `GORM`
|
||||
- Moved to maintained `Shoutrrr`: [github.com/nicholas-fedor/shoutrrr](https://github.com/nicholas-fedor/shoutrrr) ([#197](https://github.com/aceberg/WatchYourLAN/issues/197))
|
||||
|
||||
## [v0.7.2] - 2022-08-26
|
||||
### Added
|
||||
- Theme support in config
|
||||
|
||||
## [v0.7.1] - 2022-08-26
|
||||
### Added
|
||||
- Workaround for correct display of strings with apostrophes
|
||||
## [v2.1.2] - 2025-03-30
|
||||
### Fixed
|
||||
- [Issue #2](https://github.com/aceberg/WatchYourLAN/issues/2)
|
||||
- [Issue #5](https://github.com/aceberg/WatchYourLAN/issues/5)
|
||||
- Edit names bug
|
||||
- History page full rerenders replaced with only rerendering updated data
|
||||
- Select options reset
|
||||
|
||||
## [v2.1.1] - 2025-03-26
|
||||
### Fixed
|
||||
- Filter bug in Chrome
|
||||
|
||||
## [v2.1.0] - 2025-03-25
|
||||
### Added
|
||||
- Rewrited GUI in `SolidJS` and `TypeScript`
|
||||
- Prometheus integration [#181](https://github.com/aceberg/WatchYourLAN/pull/181)
|
||||
- Optimized Docker build [#180](https://github.com/aceberg/WatchYourLAN/pull/180)
|
||||
|
||||
### Fixed
|
||||
- Vite: file names
|
||||
- Node Path bug
|
||||
|
||||
## [v2.0.4] - 2024-10-21
|
||||
### Added
|
||||
- Notification test [#147](https://github.com/aceberg/WatchYourLAN/issues/147)
|
||||
- API status [#148](https://github.com/aceberg/WatchYourLAN/issues/148)
|
||||
|
||||
### Fixed
|
||||
- [#101](https://github.com/aceberg/WatchYourLAN/issues/101)
|
||||
- The same problem for Theme, Color mode, Log level
|
||||
- Sort bug in Chrome [#140](https://github.com/aceberg/WatchYourLAN/issues/140)
|
||||
|
||||
## [v2.0.3] - 2024-09-17
|
||||
### Fixed
|
||||
- `ARP_STRS_JOINED` should be empty in config file
|
||||
- Optimized History Trim
|
||||
|
||||
## [v2.0.2] - 2024-09-07
|
||||
### Added
|
||||
- Remember Refresh setting in browser [#123](https://github.com/aceberg/WatchYourLAN/issues/123)
|
||||
|
||||
### Fixed
|
||||
- Error when `IFACES` are empty
|
||||
- Sticky sort bug fix
|
||||
- Bug [#124](https://github.com/aceberg/WatchYourLAN/issues/124)
|
||||
- Bug [#128](https://github.com/aceberg/WatchYourLAN/issues/128)
|
||||
|
||||
|
||||
## [v2.0.1] - 2024-09-02
|
||||
### Added
|
||||
- `Vlans` and `docker0` support [#47](https://github.com/aceberg/WatchYourLAN/issues/47). Thanks [thehijacker](https://github.com/thehijacker)!
|
||||
- Remember `sort` field
|
||||
- `InfluxDB` error handling
|
||||
|
||||
### Fixed
|
||||
- Bug [#103](https://github.com/aceberg/WatchYourLAN/issues/103)
|
||||
- Bug [#104](https://github.com/aceberg/WatchYourLAN/issues/104). Thanks [Steve Clement](https://github.com/SteveClement)!
|
||||
|
||||
## [v2.0.0] - 2024-08-30
|
||||
### Added
|
||||
- API
|
||||
- Arguments for `arp-scan` option
|
||||
- `InfluxDB` export
|
||||
- `PostgreSQL` or `SQLite` DB options
|
||||
- Names from DNS
|
||||
|
||||
### Changed
|
||||
- Better UI with JS
|
||||
- Switched to `gin` web framework
|
||||
- Reworked DB schema and config variables
|
||||
|
||||
|
|
|
|||
20
Dockerfile
|
|
@ -1,8 +1,18 @@
|
|||
FROM golang:alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
||||
|
||||
RUN apk add build-base
|
||||
COPY . /src
|
||||
RUN cd /src/cmd/WatchYourLAN/ && CGO_ENABLED=0 go build -o /WatchYourLAN .
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
|
||||
COPY --from=xx / /
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY backend/go.mod backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY backend/ .
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN CGO_ENABLED=0 xx-go build -ldflags='-w -s' -o /WatchYourLAN ./cmd/WatchYourLAN
|
||||
|
||||
|
||||
FROM alpine
|
||||
|
|
@ -14,4 +24,4 @@ RUN apk add --no-cache arp-scan tzdata \
|
|||
|
||||
COPY --from=builder /WatchYourLAN /app/
|
||||
|
||||
ENTRYPOINT ["./WatchYourLAN"]
|
||||
ENTRYPOINT ["./WatchYourLAN"]
|
||||
|
|
|
|||
18
FAQ.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# FAQ
|
||||
|
||||
## Allow custom MAC vendor overrides
|
||||
Issues [#169](https://github.com/aceberg/WatchYourLAN/issues/169), [#185](https://github.com/aceberg/WatchYourLAN/issues/185)
|
||||
|
||||
WatchYourLAN is using `arp-scan`, so most of its options are available to WYL users.
|
||||
|
||||
1. Prepare a [mac-vendor.txt](https://manpages.debian.org/testing/arp-scan/mac-vendor.5.en.html) file with additional MACs and put it in a mounted WYL directory.
|
||||
2. If you are using `IFACES` variable to define interfaces, add path to mac-vendor.txt to `ARP_ARGS`
|
||||
```yaml
|
||||
arp_args: --macfile=/data/WatchYourLAN/mac-vendor.txt
|
||||
```
|
||||
3. For interfaces defined in `ARP_STRS` add the same directly in the beginning of `ARP_STRS` string
|
||||
```yaml
|
||||
arp_strs:
|
||||
- --macfile=/data/WatchYourLAN/mac-vendor.txt -gNx 10.144.0.1/24 -I eth0
|
||||
```
|
||||
4. **WARNING!** To see an updated vendor, you'll have to delete host and wait for the next scan.
|
||||
50
Makefile
|
|
@ -1,50 +0,0 @@
|
|||
DUSER=aceberg
|
||||
DNAME=watchyourlan
|
||||
|
||||
IFACE=enp4s0
|
||||
DBPATH=/data/hosts.db
|
||||
SHOUTRRR_URL=gotify://192.168.2.1:8083/AwQqpAae.rrl5Ob/?title=Unknown host detected&DisableTLS=yes
|
||||
|
||||
mod:
|
||||
rm go.mod || true && \
|
||||
rm go.sum || true && \
|
||||
go mod init github.com/aceberg/WatchYourLAN && \
|
||||
go mod tidy
|
||||
|
||||
run:
|
||||
cd cmd/WatchYourLAN/ && \
|
||||
sudo \
|
||||
env IFACE=$(IFACE) DBPATH=$(DBPATH) THEME=$(THEME) \
|
||||
go run . #-c /data/config #-n http://192.168.2.3:8850
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
golint ./...
|
||||
|
||||
check: fmt lint
|
||||
|
||||
go-build:
|
||||
cd cmd/WatchYourLAN/ && \
|
||||
CGO_ENABLED=0 go build -o ../../tmp/WatchYourLAN .
|
||||
|
||||
docker-build:
|
||||
docker build -t $(DUSER)/$(DNAME) .
|
||||
|
||||
docker-run:
|
||||
docker rm wyl || true
|
||||
docker run --name wyl \
|
||||
-e "IFACE=$(IFACE)" \
|
||||
-e "SHOUTRRR_URL=$(SHOUTRRR_URL)" \
|
||||
-e "TZ=Asia/Novosibirsk" \
|
||||
--network="host" \
|
||||
-v ~/.dockerdata/wyl:/data \
|
||||
$(DUSER)/$(DNAME)
|
||||
|
||||
clean:
|
||||
rm src/$(DNAME) || true
|
||||
docker rmi -f $(DUSER)/$(DNAME)
|
||||
|
||||
dev: docker-build docker-run
|
||||
217
README.md
|
|
@ -5,83 +5,178 @@
|
|||
|
||||
[](https://github.com/aceberg/WatchYourLAN/actions/workflows/main-docker-all.yml)
|
||||
[](https://goreportcard.com/report/github.com/aceberg/WatchYourLAN)
|
||||
[](https://codeclimate.com/github/aceberg/WatchYourLAN/maintainability)
|
||||

|
||||

|
||||
[](https://hub.docker.com/r/aceberg/watchyourlan)
|
||||
[](https://github.com/aceberg/WatchYourLAN/discussions)
|
||||
|
||||
Lightweight network IP scanner with web GUI
|
||||
- [Quick start](https://github.com/aceberg/WatchYourLAN#quick-start)
|
||||
- [Config](https://github.com/aceberg/WatchYourLAN#config)
|
||||
- [Config file](https://github.com/aceberg/WatchYourLAN#config-file)
|
||||
- [Options](https://github.com/aceberg/WatchYourLAN#options)
|
||||
- [Local network only](https://github.com/aceberg/WatchYourLAN#local-network-only)
|
||||
- [Thanks](https://github.com/aceberg/WatchYourLAN#thanks)
|
||||
<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
|
||||
- Monitor hosts online/offline history
|
||||
- Keep a list of all hosts in the network
|
||||
- Send data to `InfluxDB2` or `Prometheus` to make a `Grafana` dashboard
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Please, consider making a [donation](https://github.com/aceberg#donate). Even $10 will make a difference to me.
|
||||
|
||||

|
||||
|
||||
## More screenshots
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
</details>
|
||||
|
||||
## Quick start
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Replace `$YOURTIMEZONE` with correct time zone and `$YOURIFACE` with network interface you want to scan. Network mode must be `host`. Set `$DOCKERDATAPATH` for container to save data:
|
||||
|
||||
```sh
|
||||
docker run --name wyl \
|
||||
-e "IFACE=$YOURIFACE" \
|
||||
-e "IFACES=$YOURIFACE" \
|
||||
-e "TZ=$YOURTIMEZONE" \
|
||||
--network="host" \
|
||||
-v $DOCKERDATAPATH/wyl:/data \
|
||||
-v $DOCKERDATAPATH/wyl:/data/WatchYourLAN \
|
||||
aceberg/watchyourlan
|
||||
```
|
||||
Web GUI should be at http://localhost:8840
|
||||
|
||||
</details>
|
||||
|
||||
## Auth
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
**WatchYourLAN** does not have built-in auth option. But you can use it with SSO tools like Authelia, or my simple auth app [ForAuth](https://github.com/aceberg/ForAuth).
|
||||
Here is an example [docker-compose-auth.yml](https://github.com/aceberg/WatchYourLAN/blob/main/docker-compose-auth.yml).
|
||||
|
||||
> :warning: **WARNING!**
|
||||
> Please, don't forget that WYL needs `host` network mode to work. So, WYL port will be exposed in this setup. You need to limit access to it with firewall or other measures.
|
||||
|
||||
</details>
|
||||
|
||||
## Install on Linux
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
All binary packages can be found in [latest](https://github.com/aceberg/WatchYourLAN/releases/latest) release. There are `.deb`, `.rpm`, `.apk` (Alpine Linux) and `.tar.gz` files.
|
||||
|
||||
Supported architectures: `amd64`, `i386`, `arm_v5`, `arm_v6`, `arm_v7`, `arm64`.
|
||||
Dependencies: `arp-scan`, `tzdata`.
|
||||
|
||||
For `amd64` there is a `deb` repo [available](https://github.com/aceberg/ppa)
|
||||
|
||||
</details>
|
||||
|
||||
## Config
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Configuration can be done through config file, GUI or environment variables. Variable names is `config_v2.yaml` file are the same, but in lowcase.
|
||||
|
||||
Configuration can be done through config file or environment variables
|
||||
|
||||
### Basic config
|
||||
| Variable | Description | Default |
|
||||
| -------- | ----------- | ------- |
|
||||
| IFACE | Interface to scan. Could be one or more, separated by space. Currently `docker0` is not allowed, as arp-scan wouldn't work with it correctly | enp1s0 |
|
||||
| DBPATH | Path to Database | /data/db.sqlite |
|
||||
| GUIIP | Address for web GUI | localhost (127.0.0.1) |
|
||||
| GUIPORT | Port for web GUI | 8840 |
|
||||
| TIMEOUT | Time between scans (seconds) | 60 (1 minute) |
|
||||
| SHOUTRRR_URL | Url to any notification service supported by [Shoutrrr](https://github.com/containrrr/shoutrrr) (gotify, email, telegram and others) or [Generic Webhook](https://github.com/containrrr/shoutrrr/blob/main/docs/services/generic.md) | "" |
|
||||
| THEME | Any theme name from https://bootswatch.com in lowcase | solar |
|
||||
| COLOR | Background color: light or dark | light |
|
||||
| IGNOREIP | If you want to detect unknown hosts by MAC only, set this wariable to "yes" | no |
|
||||
| LOGLEVEL | How much log output you want to see ("short" or "verbose") | verbose |
|
||||
| AUTH | Enable Session-Cookie authentication | false |
|
||||
| AUTH_USER | Username | "" |
|
||||
| AUTH_PASSWORD | Encrypted password (bcrypt) | "" |
|
||||
| AUTH_EXPIRE | Session expiration time. A number and suffix: **m, h, d** or **M**. | 7d |
|
||||
| TZ | Set your timezone for correct time | |
|
||||
| HOST | Listen address | 0.0.0.0 |
|
||||
| PORT | Port for web GUI | 8840 |
|
||||
| THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) | sand |
|
||||
| COLOR | Background color: light or dark | dark |
|
||||
| NODEPATH | Path to local node modules | |
|
||||
| SHOUTRRR_URL | WatchYourLAN uses [Shoutrrr](https://github.com/nicholas-fedor/shoutrrr) to send notifications. It is already integrated, just needs a correct URL. Examples for Discord, Email, Gotify, Matrix, Ntfy, Pushover, Slack, Telegram, Generic Webhook and etc are [here](https://nicholas-fedor.github.io/shoutrrr/) | |
|
||||
|
||||
### Scan settings
|
||||
| Variable | Description | Default |
|
||||
| -------- | ----------- | ------- |
|
||||
| IFACES | Interfaces to scan. Could be one or more, separated by space. See [docs/VLAN_ARP_SCAN.md](https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md). | |
|
||||
| TIMEOUT | Time between scans (seconds) | 120 |
|
||||
| ARP_ARGS | Arguments for `arp-scan`. Enable `debug` log level to see resulting command. (Example: `-r 1`). See [docs/VLAN_ARP_SCAN.md](https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md). | |
|
||||
| ARP_STRS ARP_STRS_JOINED | See [docs/VLAN_ARP_SCAN.md](https://github.com/aceberg/WatchYourLAN/blob/main/docs/VLAN_ARP_SCAN.md). | |
|
||||
| LOG_LEVEL | Log level: `debug`, `info`, `warn` or `error` | info |
|
||||
| TRIM_HIST | Remove history after (hours) | 48 |
|
||||
| HIST_IN_DB | DEPRECATED since 2.1.3. Now History is always stored in DB. Use TRIM_HIST to reduce DB size | |
|
||||
| USE_DB | Either `sqlite` or `postgres` | sqlite |
|
||||
| PG_CONNECT | Address to connect to PostgreSQL. (Example: `postgres://username:password@192.168.0.1:5432/dbname?sslmode=disable`). Full list of URL parameters [here](https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters) | |
|
||||
|
||||
### InfluxDB2 config
|
||||
This config matches Grafana's config for InfluxDB data source
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
| -------- | ----------- | ------- | ------- |
|
||||
| INFLUX_ENABLE | Enable export to InfluxDB2 | false | true |
|
||||
| INFLUX_SKIP_TLS | Skip TLS Verify | false | true |
|
||||
| INFLUX_ADDR | Address:port of InfluxDB2 server | | https://192.168.2.3:8086/ |
|
||||
| INFLUX_BUCKET | InfluxDB2 bucket | | test |
|
||||
| INFLUX_ORG | InfluxDB2 org | | home |
|
||||
| INFLUX_TOKEN | Secret token, generated by InfluxDB2 | | |
|
||||
|
||||
### Prometheus config
|
||||
This config configures the Prometheus data source
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
| -------- | ----------- | ------- | ------- |
|
||||
| PROMETHEUS_ENABLE | Enable the Prometheus `/metrics` endpoint | false | true |
|
||||
|
||||
</details>
|
||||
|
||||
## Config file
|
||||
> [!WARNING]
|
||||
> Config file format has been migrated to YAML in release [v1.0.0](https://github.com/aceberg/WatchYourLAN/releases/tag/1.0.0).
|
||||
|
||||
Config file path is `/data/config.yaml`.
|
||||
All variables could be set there. Example:
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Config file name is `config_v2.yaml`. Example:
|
||||
|
||||
```yaml
|
||||
color: light
|
||||
dbpath: /data/db.sqlite
|
||||
guiip: 192.168.2.1
|
||||
guiport: "8840"
|
||||
iface: enp1s0
|
||||
ignoreip: "no"
|
||||
loglevel: short
|
||||
shoutrrr_url: gotify://192.168.2.1:8083/AwQqpAae.rrl5Ob/?title=Unknown host detected&DisableTLS=yes
|
||||
theme: solar
|
||||
timeout: 120
|
||||
arp_args: ""
|
||||
color: dark
|
||||
host: 0.0.0.0
|
||||
ifaces: enp4s0
|
||||
influx_addr: ""
|
||||
influx_bucket: ""
|
||||
influx_enable: false
|
||||
influx_org: ""
|
||||
influx_skip_tls: false
|
||||
influx_token: ""
|
||||
log_level: info
|
||||
nodepath: ""
|
||||
pg_connect: ""
|
||||
port: "8840"
|
||||
prometheus_enable: false
|
||||
shoutrrr_url: "gotify://192.168.0.1:8083/AwQqpAae.rrl5Ob/?title=Unknown host detected&DisableTLS=yes"
|
||||
theme: sand
|
||||
timeout: 60
|
||||
trim_hist: 48
|
||||
use_db: sqlite
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Options
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
| Key | Description | Default |
|
||||
| -------- | ----------- | ------- |
|
||||
| -c | Path to config file | /data/config.yaml |
|
||||
| -n | Path to node modules (see below) | "" |
|
||||
| -d | Path to config dir | /data/WatchYourLAN |
|
||||
| -n | Path to node modules (see below) | |
|
||||
|
||||
</details>
|
||||
|
||||
## Local network only
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
By default, this app pulls themes, icons and fonts from the internet. But, in some cases, it may be useful to have an independent from global network setup. I created a separate [image](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) with all necessary modules and fonts.
|
||||
Run with Docker:
|
||||
```sh
|
||||
|
|
@ -91,16 +186,38 @@ docker run --name node-bootstrap \
|
|||
```
|
||||
```sh
|
||||
docker run --name wyl \
|
||||
-e "IFACE=$YOURIFACE" \
|
||||
-e "IFACES=$YOURIFACE" \
|
||||
-e "TZ=$YOURTIMEZONE" \
|
||||
--network="host" \
|
||||
-v $DOCKERDATAPATH/wyl:/data \
|
||||
aceberg/watchyourlan -n "http://127.0.0.1:8850"
|
||||
-v $DOCKERDATAPATH/wyl:/data/WatchYourLAN \
|
||||
aceberg/watchyourlan -n "http://$YOUR_IP:8850"
|
||||
```
|
||||
Or use [docker-compose](docker-compose-local.yml)
|
||||
Or use [docker-compose](docker-compose.yml)
|
||||
|
||||
</details>
|
||||
|
||||
## API & Integrations
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
### API
|
||||
Moved to [docs/API.md](https://github.com/aceberg/WatchYourLAN/blob/main/docs/API.md)
|
||||
|
||||
### Integrations
|
||||
- [ArchLinux (AUR)](https://aur.archlinux.org/packages/watch-your-lan) by `gilcu3`
|
||||
- [Python API client](https://github.com/drwahl/py-watchyourlanclient) by [drwahl](https://github.com/drwahl)
|
||||
- [Umbrel](https://apps.umbrel.com/app/watch-your-lan) by [Jasper](https://github.com/ceramicwhite)
|
||||
- [YunoHost](https://apps.yunohost.org/app/watchyourlan)
|
||||
</details>
|
||||
|
||||
## Thanks
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
- All go packages listed in [dependencies](https://github.com/aceberg/WatchYourLAN/network/dependencies)
|
||||
- Favicon and logo: [Access point icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/access-point)
|
||||
- [Bootstrap](https://getbootstrap.com/)
|
||||
- Themes: [Free themes for Bootstrap](https://bootswatch.com)
|
||||
- Themes: [Free themes for Bootstrap](https://bootswatch.com)
|
||||
|
||||
</details>
|
||||
|
|
|
|||
BIN
assets/Screenshot 2024-08-29 at 01-41-12 WatchYourLAN.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/Screenshot 2024-08-29 at 11-17-59 WatchYourLAN.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/Screenshot_1.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/Screenshot_2.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
assets/Screenshot_3.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
assets/Screenshot_4.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/Screenshot_5.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
67
backend/.goreleaser.yaml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
version: 2
|
||||
project_name: watchyourlan
|
||||
builds:
|
||||
- main: ./cmd/WatchYourLAN/
|
||||
binary: watchyourlan
|
||||
id: default
|
||||
env: [CGO_ENABLED=0]
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
|
||||
nfpms:
|
||||
- id: systemd
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
maintainer: aceberg <aceberg_a@proton.me>
|
||||
description: Lightweight network IP scanner with web GUI
|
||||
homepage: https://github.com/aceberg/watchyourlan
|
||||
license: MIT
|
||||
section: utils
|
||||
dependencies: # Don't forget to edit!
|
||||
- arp-scan
|
||||
- tzdata
|
||||
contents:
|
||||
- src: ./configs/watchyourlan.service
|
||||
dst: /lib/systemd/system/watchyourlan.service
|
||||
scripts:
|
||||
postinstall: ./configs/postinstall.sh
|
||||
|
||||
- id: alpine
|
||||
formats:
|
||||
- apk
|
||||
maintainer: aceberg <aceberg_a@proton.me>
|
||||
description: Lightweight network IP scanner with web GUI
|
||||
homepage: https://github.com/aceberg/watchyourlan
|
||||
license: MIT
|
||||
section: utils
|
||||
dependencies: # Don't forget to edit!
|
||||
- arp-scan
|
||||
- tzdata
|
||||
contents:
|
||||
- src: ./configs/watchyourlan
|
||||
dst: /etc/init.d/watchyourlan
|
||||
|
||||
archives:
|
||||
- files:
|
||||
- LICENSE
|
||||
- src: ./configs/watchyourlan.service
|
||||
dst: watchyourlan.service
|
||||
- src: ./configs/install.sh
|
||||
dst: install.sh
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
21
backend/LICENSE
Normal 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.
|
||||
22
backend/Makefile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
mod:
|
||||
rm go.mod || true && \
|
||||
rm go.sum || true && \
|
||||
go mod init github.com/aceberg/WatchYourLAN && \
|
||||
go mod tidy
|
||||
|
||||
run:
|
||||
cd cmd/WatchYourLAN/ && \
|
||||
sudo \
|
||||
go run . #-n http://192.168.2.3:8850
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
golint ./...
|
||||
|
||||
check: fmt lint
|
||||
|
||||
swag:
|
||||
swag init -g main.go --dir cmd/WatchYourLAN,internal/api,internal/models
|
||||
52
backend/cmd/WatchYourLAN/main.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// @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 (
|
||||
"flag"
|
||||
// "net/http"
|
||||
|
||||
// _ "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"
|
||||
"github.com/aceberg/WatchYourLAN/internal/web"
|
||||
)
|
||||
|
||||
const dirPath = "/data/WatchYourLAN"
|
||||
const nodePath = ""
|
||||
|
||||
func main() {
|
||||
dirPtr := flag.String("d", dirPath, "Path to config dir")
|
||||
nodePtr := flag.String("n", nodePath, "Path to node modules")
|
||||
flag.Parse()
|
||||
|
||||
// pprof - memory leak detect
|
||||
// go tool pprof -alloc_space http://localhost:8085/debug/pprof/heap
|
||||
// (pprof) web
|
||||
// (pprof) list db.Select
|
||||
//
|
||||
// go func() {
|
||||
// http.ListenAndServe("localhost:8085", nil)
|
||||
// }()
|
||||
|
||||
// Make AppConfig
|
||||
conf.Start(*dirPtr, *nodePtr)
|
||||
|
||||
gdb.Start()
|
||||
|
||||
routines.ScanRestart()
|
||||
routines.HistoryTrim()
|
||||
|
||||
web.Gui()
|
||||
}
|
||||
3
backend/configs/postinstall.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
systemctl daemon-reload
|
||||
6
backend/configs/watchyourlan
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/sbin/openrc-run
|
||||
name="WatchYourLAN"
|
||||
description="Lightweight network IP scanner with web GUI"
|
||||
command="/usr/bin/watchyourlan"
|
||||
command_background=true
|
||||
pidfile="/run/watchyourlan.pid"
|
||||
|
|
@ -5,7 +5,7 @@ After=network-online.target
|
|||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/watchyourlan -c /etc/watchyourlan/config.yaml
|
||||
ExecStart=/usr/bin/watchyourlan -d /etc/watchyourlan/
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
630
backend/docs/docs.go
Normal 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
|
|
@ -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
|
|
@ -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"
|
||||
95
backend/go.mod
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
module github.com/aceberg/WatchYourLAN
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
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/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.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
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
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.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
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
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
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
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.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.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.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
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // 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
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
)
|
||||
302
backend/go.sum
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
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 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=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
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=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
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.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=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
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.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-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=
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
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.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=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
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.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.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.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.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.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=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.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/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.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.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.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.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=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
60
backend/internal/api/api-history.go
Normal 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)
|
||||
}
|
||||
120
backend/internal/api/api-hosts.go
Normal 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")
|
||||
}
|
||||
51
backend/internal/api/api-network.go
Normal 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))
|
||||
}
|
||||
104
backend/internal/api/api-system.go
Normal 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)
|
||||
}
|
||||
88
backend/internal/api/config.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/gdb"
|
||||
"github.com/aceberg/WatchYourLAN/internal/routines"
|
||||
)
|
||||
|
||||
func saveConfigHandler(c *gin.Context) {
|
||||
|
||||
conf.AppConfig.Host = c.PostForm("host")
|
||||
conf.AppConfig.Port = c.PostForm("port")
|
||||
conf.AppConfig.Theme = c.PostForm("theme")
|
||||
conf.AppConfig.Color = c.PostForm("color")
|
||||
conf.AppConfig.NodePath = c.PostForm("node")
|
||||
conf.AppConfig.ShoutURL = c.PostForm("shout")
|
||||
|
||||
conf.Write(conf.AppConfig)
|
||||
|
||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||
}
|
||||
|
||||
func saveSettingsHandler(c *gin.Context) {
|
||||
|
||||
conf.AppConfig.LogLevel = c.PostForm("log")
|
||||
conf.AppConfig.ArpArgs = c.PostForm("arpargs")
|
||||
conf.AppConfig.Ifaces = c.PostForm("ifaces")
|
||||
|
||||
useDB := c.PostForm("usedb")
|
||||
pgConnect := c.PostForm("pgconnect")
|
||||
|
||||
if useDB != conf.AppConfig.UseDB || pgConnect != conf.AppConfig.PGConnect {
|
||||
conf.AppConfig.UseDB = c.PostForm("usedb")
|
||||
conf.AppConfig.PGConnect = c.PostForm("pgconnect")
|
||||
gdb.Connect()
|
||||
}
|
||||
|
||||
timeout := c.PostForm("timeout")
|
||||
trimHist := c.PostForm("trim")
|
||||
conf.AppConfig.Timeout, _ = strconv.Atoi(timeout)
|
||||
conf.AppConfig.TrimHist, _ = strconv.Atoi(trimHist)
|
||||
|
||||
arpStrs := c.PostFormArray("arpstrs")
|
||||
conf.AppConfig.ArpStrs = []string{}
|
||||
for _, s := range arpStrs {
|
||||
if s != "" {
|
||||
conf.AppConfig.ArpStrs = append(conf.AppConfig.ArpStrs, s)
|
||||
}
|
||||
}
|
||||
|
||||
conf.Write(conf.AppConfig)
|
||||
|
||||
routines.ScanRestart()
|
||||
|
||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||
}
|
||||
|
||||
func saveInfluxHandler(c *gin.Context) {
|
||||
|
||||
conf.AppConfig.InfluxAddr = c.PostForm("addr")
|
||||
conf.AppConfig.InfluxToken = c.PostForm("token")
|
||||
conf.AppConfig.InfluxOrg = c.PostForm("org")
|
||||
conf.AppConfig.InfluxBucket = c.PostForm("bucket")
|
||||
|
||||
enable := c.PostForm("enable")
|
||||
skip := c.PostForm("skip")
|
||||
conf.AppConfig.InfluxEnable = enable == "on"
|
||||
conf.AppConfig.InfluxSkipTLS = skip == "on"
|
||||
|
||||
conf.Write(conf.AppConfig)
|
||||
|
||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||
}
|
||||
|
||||
func savePrometheusHandler(c *gin.Context) {
|
||||
enable := c.PostForm("enable")
|
||||
|
||||
conf.AppConfig.PrometheusEnable = enable == "on"
|
||||
|
||||
conf.Write(conf.AppConfig)
|
||||
|
||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||
}
|
||||
16
backend/internal/api/functions.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/gdb"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
func getHostByID(idStr string) (oneHost models.Host) {
|
||||
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
oneHost = gdb.SelectByID(id)
|
||||
|
||||
return oneHost
|
||||
}
|
||||
41
backend/internal/api/routes.go
Normal 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))
|
||||
}
|
||||
98
backend/internal/arp/arpscan.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package arp
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
var arpArgs string
|
||||
|
||||
func scanIface(iface string) string {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if arpArgs != "" {
|
||||
cmd = exec.Command("arp-scan", "-glNx", arpArgs, "-I", iface)
|
||||
} else {
|
||||
cmd = exec.Command("arp-scan", "-glNx", "-I", iface)
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
slog.Debug(cmd.String())
|
||||
|
||||
if check.IfError(err) {
|
||||
return string("")
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func scanStr(str string) string {
|
||||
|
||||
args := strings.Split(str, " ")
|
||||
cmd := exec.Command("arp-scan", args...)
|
||||
|
||||
out, err := cmd.Output()
|
||||
slog.Debug(cmd.String())
|
||||
|
||||
if check.IfError(err) {
|
||||
return string("")
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func parseOutput(text, iface string) []models.Host {
|
||||
var foundHosts = []models.Host{}
|
||||
|
||||
p := strings.Split(text, "\n")
|
||||
|
||||
for _, host := range p {
|
||||
if host != "" {
|
||||
var oneHost models.Host
|
||||
p := strings.Split(host, " ")
|
||||
oneHost.Iface = iface
|
||||
oneHost.IP = p[0]
|
||||
oneHost.Mac = p[1]
|
||||
oneHost.Hw = p[2]
|
||||
oneHost.Date = time.Now().Format("2006-01-02 15:04:05")
|
||||
oneHost.Now = 1
|
||||
foundHosts = append(foundHosts, oneHost)
|
||||
}
|
||||
}
|
||||
|
||||
return foundHosts
|
||||
}
|
||||
|
||||
// Scan all interfaces
|
||||
func Scan(ifaces, args string, strs []string) []models.Host {
|
||||
var text string
|
||||
var p []string
|
||||
var foundHosts = []models.Host{}
|
||||
arpArgs = args
|
||||
|
||||
if ifaces != "" {
|
||||
|
||||
p = strings.Split(ifaces, " ")
|
||||
|
||||
for _, iface := range p {
|
||||
slog.Debug("Scanning interface " + iface)
|
||||
text = scanIface(iface)
|
||||
slog.Debug("Found IPs: \n" + text)
|
||||
|
||||
foundHosts = append(foundHosts, parseOutput(text, iface)...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range strs {
|
||||
slog.Debug("Scanning string " + s)
|
||||
text = scanStr(s)
|
||||
slog.Debug("Found IPs: \n" + text)
|
||||
p = strings.Split(s, " ")
|
||||
|
||||
foundHosts = append(foundHosts, parseOutput(text, p[len(p)-1])...)
|
||||
}
|
||||
|
||||
return foundHosts
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package check
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// IfError prints error, if it is not nil
|
||||
|
|
@ -10,6 +11,6 @@ func IfError(err error) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
log.Println("ERROR:", err)
|
||||
slog.Error(fmt.Sprintf("%v", err))
|
||||
return true
|
||||
}
|
||||
21
backend/internal/check/network.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package check
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// DNS - returns DNS names of a host
|
||||
func DNS(host models.Host) (name, dns string) {
|
||||
|
||||
dnsNames, _ := net.LookupAddr(host.IP)
|
||||
|
||||
if len(dnsNames) > 0 {
|
||||
name = dnsNames[0]
|
||||
dns = strings.Join(dnsNames, " ")
|
||||
}
|
||||
|
||||
return name, dns
|
||||
}
|
||||
74
backend/internal/conf/read.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
func read(path string) (config models.Conf) {
|
||||
|
||||
viper.SetDefault("HOST", "0.0.0.0")
|
||||
viper.SetDefault("PORT", "8840")
|
||||
viper.SetDefault("THEME", "sand")
|
||||
viper.SetDefault("COLOR", "dark")
|
||||
viper.SetDefault("NODEPATH", "")
|
||||
viper.SetDefault("LOG_LEVEL", "info")
|
||||
viper.SetDefault("ARP_ARGS", "")
|
||||
viper.SetDefault("ARP_STRS_JOINED", "")
|
||||
viper.SetDefault("IFACES", "")
|
||||
viper.SetDefault("TIMEOUT", 120)
|
||||
viper.SetDefault("TRIM_HIST", 48)
|
||||
viper.SetDefault("SHOUTRRR_URL", "")
|
||||
|
||||
viper.SetDefault("USE_DB", "sqlite")
|
||||
viper.SetDefault("PG_CONNECT", "")
|
||||
|
||||
viper.SetDefault("INFLUX_ENABLE", false)
|
||||
|
||||
viper.SetDefault("PROMETHEUS_ENABLE", false)
|
||||
|
||||
viper.SetConfigFile(path)
|
||||
viper.SetConfigType("yaml")
|
||||
err := viper.ReadInConfig()
|
||||
check.IfError(err)
|
||||
|
||||
viper.AutomaticEnv() // Get ENVIRONMENT variables
|
||||
|
||||
config.Host = viper.Get("HOST").(string)
|
||||
config.Port = viper.Get("PORT").(string)
|
||||
config.Theme = viper.Get("THEME").(string)
|
||||
config.Color = viper.Get("COLOR").(string)
|
||||
config.NodePath = viper.Get("NODEPATH").(string)
|
||||
config.LogLevel = viper.Get("LOG_LEVEL").(string)
|
||||
config.ArpArgs = viper.Get("ARP_ARGS").(string)
|
||||
config.ArpStrs = viper.GetStringSlice("ARP_STRS")
|
||||
config.Ifaces = viper.Get("IFACES").(string)
|
||||
config.Timeout = viper.GetInt("TIMEOUT")
|
||||
config.TrimHist = viper.GetInt("TRIM_HIST")
|
||||
config.ShoutURL = viper.Get("SHOUTRRR_URL").(string)
|
||||
|
||||
config.UseDB = viper.Get("USE_DB").(string)
|
||||
config.PGConnect = viper.Get("PG_CONNECT").(string)
|
||||
|
||||
config.InfluxEnable = viper.GetBool("INFLUX_ENABLE")
|
||||
config.InfluxSkipTLS = viper.GetBool("INFLUX_SKIP_TLS")
|
||||
config.InfluxAddr, _ = viper.Get("INFLUX_ADDR").(string)
|
||||
config.InfluxToken, _ = viper.Get("INFLUX_TOKEN").(string)
|
||||
config.InfluxOrg, _ = viper.Get("INFLUX_ORG").(string)
|
||||
config.InfluxBucket, _ = viper.Get("INFLUX_BUCKET").(string)
|
||||
|
||||
config.PrometheusEnable = viper.GetBool("PROMETHEUS_ENABLE")
|
||||
|
||||
joined := viper.Get("ARP_STRS_JOINED").(string)
|
||||
// slog.Info("ARP_STRS_JOINED: " + joined)
|
||||
|
||||
if joined != "" {
|
||||
config.ArpStrs = strings.Split(joined, ",")
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
25
backend/internal/conf/start.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// AppConfig - app config
|
||||
var AppConfig models.Conf
|
||||
|
||||
// Start - initial config
|
||||
func Start(dirPath, nodePath string) {
|
||||
|
||||
confPath := dirPath + "/config_v2.yaml"
|
||||
check.Path(confPath)
|
||||
|
||||
AppConfig = read(confPath)
|
||||
|
||||
AppConfig.DirPath = dirPath
|
||||
AppConfig.ConfPath = confPath
|
||||
AppConfig.DBPath = dirPath + "/scan.db"
|
||||
if nodePath != "" {
|
||||
AppConfig.NodePath = nodePath
|
||||
}
|
||||
}
|
||||
48
backend/internal/conf/write.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// Write - write config to file
|
||||
func Write(config models.Conf) {
|
||||
|
||||
slog.Info("Writing new config to " + config.ConfPath)
|
||||
|
||||
viper.SetConfigFile(config.ConfPath)
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
viper.Set("HOST", config.Host)
|
||||
viper.Set("PORT", config.Port)
|
||||
viper.Set("THEME", config.Theme)
|
||||
viper.Set("COLOR", config.Color)
|
||||
viper.Set("NODEPATH", config.NodePath)
|
||||
viper.Set("LOG_LEVEL", config.LogLevel)
|
||||
viper.Set("ARP_ARGS", config.ArpArgs)
|
||||
viper.Set("ARP_STRS", config.ArpStrs)
|
||||
viper.Set("ARP_STRS_JOINED", "") // Can be set only with ENV
|
||||
viper.Set("IFACES", config.Ifaces)
|
||||
viper.Set("TIMEOUT", config.Timeout)
|
||||
viper.Set("TRIM_HIST", config.TrimHist)
|
||||
viper.Set("SHOUTRRR_URL", config.ShoutURL)
|
||||
|
||||
viper.Set("USE_DB", config.UseDB)
|
||||
viper.Set("PG_CONNECT", config.PGConnect)
|
||||
|
||||
viper.Set("influx_enable", config.InfluxEnable)
|
||||
viper.Set("influx_skip_tls", config.InfluxSkipTLS)
|
||||
viper.Set("influx_addr", config.InfluxAddr)
|
||||
viper.Set("influx_token", config.InfluxToken)
|
||||
viper.Set("influx_org", config.InfluxOrg)
|
||||
viper.Set("influx_bucket", config.InfluxBucket)
|
||||
|
||||
viper.Set("PROMETHEUS_ENABLE", config.PrometheusEnable)
|
||||
|
||||
err := viper.WriteConfig()
|
||||
check.IfError(err)
|
||||
}
|
||||
40
backend/internal/gdb/edit.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package gdb
|
||||
|
||||
import (
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// Update - update or create host
|
||||
func Update(table string, oneHost models.Host) {
|
||||
|
||||
tab := db.Table(table)
|
||||
result := tab.Save(&oneHost)
|
||||
check.IfError(result.Error)
|
||||
}
|
||||
|
||||
// Delete - delete host from DB
|
||||
func Delete(table string, id int) {
|
||||
|
||||
tab := db.Table(table)
|
||||
result := tab.Delete(&models.Host{}, id)
|
||||
check.IfError(result.Error)
|
||||
}
|
||||
|
||||
// DeleteOldHistory - delete a list of hosts from History
|
||||
func DeleteOldHistory(date string) int64 {
|
||||
|
||||
tab := db.Table("history")
|
||||
result := tab.Where("\"DATE\" < ?", date).Delete(&models.Host{})
|
||||
check.IfError(result.Error)
|
||||
|
||||
return result.RowsAffected
|
||||
}
|
||||
|
||||
// Clear - delete all hosts from table
|
||||
func Clear(table string) {
|
||||
|
||||
tab := db.Table(table)
|
||||
result := tab.Where("1 = 1").Delete(&models.Host{})
|
||||
check.IfError(result.Error)
|
||||
}
|
||||
58
backend/internal/gdb/select.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package gdb
|
||||
|
||||
import (
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// Select - get all hosts
|
||||
func Select(table string) (dbHosts []models.Host, ok bool) {
|
||||
|
||||
tab := db.Table(table)
|
||||
err := tab.Find(&dbHosts).Error
|
||||
|
||||
return dbHosts, !check.IfError(err)
|
||||
}
|
||||
|
||||
// SelectByID - get host by ID
|
||||
func SelectByID(id int) (host models.Host) {
|
||||
|
||||
tab := db.Table("now")
|
||||
tab.First(&host, id)
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// SelectByMAC - get all hosts by MAC
|
||||
func SelectByMAC(table, mac string) (hosts []models.Host) {
|
||||
|
||||
tab := db.Table(table)
|
||||
tab.Where("\"MAC\" = ?", mac).Find(&hosts)
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// SelectByDate - get all hosts by MAC and DATE
|
||||
func SelectByDate(mac, date string) (hosts []models.Host) {
|
||||
|
||||
tab := db.Table("history")
|
||||
tab.
|
||||
Where("\"MAC\" = ?", mac).
|
||||
Where("\"DATE\" LIKE ?", date+"%").
|
||||
Find(&hosts)
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// SelectLatest - get latest hosts by MAC
|
||||
func SelectLatest(mac string, number int) (hosts []models.Host) {
|
||||
|
||||
tab := db.Table("history")
|
||||
tab.
|
||||
Where("\"MAC\" = ?", mac).
|
||||
Order("\"DATE\" DESC").
|
||||
Limit(number).
|
||||
Find(&hosts)
|
||||
|
||||
return hosts
|
||||
}
|
||||
86
backend/internal/gdb/start.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package gdb
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
sqlite "github.com/aceberg/gorm-sqlite"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
var gormConf *gorm.Config
|
||||
|
||||
// Start working with DB
|
||||
func Start() {
|
||||
var tab *gorm.DB
|
||||
var err error
|
||||
|
||||
newLogger := logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: 5 * time.Second,
|
||||
LogLevel: logger.Warn,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: true,
|
||||
},
|
||||
)
|
||||
gormConf = &gorm.Config{
|
||||
Logger: newLogger,
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
NoLowerCase: true,
|
||||
// So upper case Columns could work in both PostgreSQL and SQLite
|
||||
},
|
||||
}
|
||||
|
||||
Connect()
|
||||
|
||||
// Migrate the schema
|
||||
tab = db.Table("now")
|
||||
err = tab.AutoMigrate(&models.Host{})
|
||||
check.IfError(err)
|
||||
|
||||
tab = db.Table("history")
|
||||
err = tab.AutoMigrate(&models.Host{})
|
||||
check.IfError(err)
|
||||
}
|
||||
|
||||
// Connect - choose DB and connect
|
||||
func Connect() {
|
||||
var err error
|
||||
var pgFail bool
|
||||
|
||||
if conf.AppConfig.UseDB == "postgres" {
|
||||
db, err = gorm.Open(postgres.Open(conf.AppConfig.PGConnect), gormConf)
|
||||
|
||||
if err != nil {
|
||||
pgFail = true
|
||||
|
||||
slog.Error("PostgreSQL connection error:", "err", err)
|
||||
slog.Warn("Falling back to SQLite")
|
||||
} else {
|
||||
slog.Info("Connected to DB: PostgreSQL")
|
||||
}
|
||||
}
|
||||
|
||||
if pgFail || conf.AppConfig.UseDB != "postgres" {
|
||||
|
||||
db, err = gorm.Open(sqlite.Open(conf.AppConfig.DBPath), gormConf)
|
||||
|
||||
if !check.IfError(err) {
|
||||
slog.Info("Connected to DB: SQLite")
|
||||
db.Exec("PRAGMA journal_mode = wal;")
|
||||
db.Exec("PRAGMA busy_timeout = 5000;")
|
||||
}
|
||||
}
|
||||
}
|
||||
51
backend/internal/influx/influx.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/influxdb-client-go/v2"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// Add - write data to InfluxDB2
|
||||
func Add(appConfig models.Conf, oneHist models.Host) {
|
||||
var ctx context.Context
|
||||
|
||||
client := influxdb2.NewClientWithOptions(appConfig.InfluxAddr, appConfig.InfluxToken,
|
||||
influxdb2.DefaultOptions().
|
||||
SetUseGZip(true).
|
||||
SetTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: appConfig.InfluxSkipTLS,
|
||||
}))
|
||||
|
||||
ctx = context.Background()
|
||||
ping, err := client.Ping(ctx)
|
||||
if ping {
|
||||
writeAPI := client.WriteAPIBlocking(appConfig.InfluxOrg, appConfig.InfluxBucket)
|
||||
|
||||
// Escape special characters in strings
|
||||
oneHist.Name = strings.ReplaceAll(oneHist.Name, " ", "\\ ")
|
||||
oneHist.Name = strings.ReplaceAll(oneHist.Name, ",", "\\,")
|
||||
oneHist.Name = strings.ReplaceAll(oneHist.Name, "=", "\\=")
|
||||
if oneHist.Name == "" {
|
||||
oneHist.Name = "unknown"
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("WatchYourLAN,IP=%s,iface=%s,name=%s,mac=%s,known=%d state=%d", oneHist.IP, oneHist.Iface, oneHist.Name, oneHist.Mac, oneHist.Known, oneHist.Now)
|
||||
// slog.Debug("Writing to InfluxDB", "line", line)
|
||||
|
||||
err = writeAPI.WriteRecord(context.Background(), line)
|
||||
check.IfError(err)
|
||||
} else {
|
||||
slog.Error("Can't connect to InfluxDB server")
|
||||
check.IfError(err)
|
||||
}
|
||||
|
||||
client.Close()
|
||||
}
|
||||
56
backend/internal/models/models.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package models
|
||||
|
||||
// Conf - app config
|
||||
type Conf struct {
|
||||
Host string
|
||||
Port string
|
||||
Theme string
|
||||
Color string
|
||||
DirPath string
|
||||
ConfPath string
|
||||
DBPath string
|
||||
NodePath string
|
||||
LogLevel string
|
||||
Ifaces string
|
||||
ArpArgs string
|
||||
ArpStrs []string
|
||||
Timeout int
|
||||
TrimHist int
|
||||
ShoutURL string
|
||||
Version string
|
||||
// PostgreSQL
|
||||
UseDB string
|
||||
PGConnect string
|
||||
// InfluxDB
|
||||
InfluxEnable bool
|
||||
InfluxAddr string
|
||||
InfluxToken string
|
||||
InfluxOrg string
|
||||
InfluxBucket string
|
||||
InfluxSkipTLS bool
|
||||
// Prometheus
|
||||
PrometheusEnable bool
|
||||
}
|
||||
|
||||
// Host - one host
|
||||
type Host struct {
|
||||
ID int `gorm:"column:ID;primaryKey"`
|
||||
Name string `gorm:"column:NAME"`
|
||||
DNS string `gorm:"column:DNS"`
|
||||
Iface string `gorm:"column:IFACE"`
|
||||
IP string `gorm:"column:IP"`
|
||||
Mac string `gorm:"column:MAC"`
|
||||
Hw string `gorm:"column:HW"`
|
||||
Date string `gorm:"column:DATE"`
|
||||
Known int `gorm:"column:KNOWN"`
|
||||
Now int `gorm:"column:NOW"`
|
||||
}
|
||||
|
||||
// Stat - status
|
||||
type Stat struct {
|
||||
Total int
|
||||
Online int
|
||||
Offline int
|
||||
Known int
|
||||
Unknown int
|
||||
}
|
||||
43
backend/internal/notify/shout.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// Unknown - send message to log and shoutrrr
|
||||
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)
|
||||
}
|
||||
|
||||
// Test Shoutrrr notification
|
||||
func Test() {
|
||||
|
||||
msg := "test notification"
|
||||
slog.Info("Sending " + msg)
|
||||
shout(msg)
|
||||
}
|
||||
|
||||
// shout - send msg to Shoutrrr
|
||||
func shout(msg string) {
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
wyl := "WatchYourLAN on '" + hostname + "': "
|
||||
|
||||
if conf.AppConfig.ShoutURL != "" {
|
||||
err := shoutrrr.Send(conf.AppConfig.ShoutURL, wyl+msg)
|
||||
if err != nil {
|
||||
slog.Error("Notification failed (shoutrrr): ", "", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
25
backend/internal/portscan/scan.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package portscan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IsOpen - check one tcp port
|
||||
func IsOpen(host, port string) bool {
|
||||
|
||||
timeout := 3 * time.Second
|
||||
target := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||||
|
||||
if err == nil {
|
||||
err = conn.Close()
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
47
backend/internal/prometheus/prometheus.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package prometheus
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
// Handler - display Prometheus metrics
|
||||
func Handler() func(c *gin.Context) {
|
||||
h := promhttp.Handler()
|
||||
return func(c *gin.Context) {
|
||||
if !conf.AppConfig.PrometheusEnable {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
||||
var up = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "watch_your_lan",
|
||||
Name: "up",
|
||||
Help: "Whether the host is up (1 for yes, 0 for no)",
|
||||
}, []string{"ip", "iface", "name", "mac", "known"})
|
||||
|
||||
// Add a Prometheus metric
|
||||
func Add(oneHist models.Host) {
|
||||
if oneHist.Name == "" {
|
||||
oneHist.Name = "unknown"
|
||||
}
|
||||
|
||||
up.With(prometheus.Labels{
|
||||
"ip": oneHist.IP,
|
||||
"iface": oneHist.Iface,
|
||||
"name": oneHist.Name,
|
||||
"mac": oneHist.Mac,
|
||||
"known": strconv.Itoa(oneHist.Known),
|
||||
}).Set(float64(oneHist.Now))
|
||||
}
|
||||
44
backend/internal/routines/restart-scan.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package routines
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
)
|
||||
|
||||
var (
|
||||
quitScan = make(chan bool)
|
||||
)
|
||||
|
||||
// ScanRestart - start or update routines
|
||||
func ScanRestart() {
|
||||
|
||||
close(quitScan)
|
||||
|
||||
slog.Info("Restarting scan routine")
|
||||
setLogLevel()
|
||||
|
||||
quitScan = make(chan bool)
|
||||
go startScan(quitScan) // scan-routine.go
|
||||
}
|
||||
|
||||
func setLogLevel() {
|
||||
var level slog.Level
|
||||
|
||||
slog.Info("Log level: " + conf.AppConfig.LogLevel)
|
||||
|
||||
switch conf.AppConfig.LogLevel {
|
||||
case "debug":
|
||||
level = slog.LevelDebug
|
||||
case "info":
|
||||
level = slog.LevelInfo
|
||||
case "warn":
|
||||
level = slog.LevelWarn
|
||||
case "error":
|
||||
level = slog.LevelError
|
||||
default:
|
||||
slog.Error("Invalid log level. Setting default level INFO")
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
slog.SetLogLoggerLevel(level)
|
||||
}
|
||||
91
backend/internal/routines/scan-routine.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package routines
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/arp"
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/gdb"
|
||||
"github.com/aceberg/WatchYourLAN/internal/influx"
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
"github.com/aceberg/WatchYourLAN/internal/notify"
|
||||
"github.com/aceberg/WatchYourLAN/internal/prometheus"
|
||||
)
|
||||
|
||||
func startScan(quit chan bool) {
|
||||
var lastDate, nowDate, plusDate time.Time
|
||||
var foundHosts []models.Host
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
default:
|
||||
nowDate = time.Now()
|
||||
plusDate = lastDate.Add(time.Duration(conf.AppConfig.Timeout) * time.Second)
|
||||
|
||||
if nowDate.After(plusDate) {
|
||||
|
||||
foundHosts = arp.Scan(conf.AppConfig.Ifaces, conf.AppConfig.ArpArgs, conf.AppConfig.ArpStrs)
|
||||
|
||||
// Make map of found hosts
|
||||
foundHostsMap := make(map[string]models.Host)
|
||||
for _, fHost := range foundHosts {
|
||||
foundHostsMap[fHost.Mac] = fHost
|
||||
}
|
||||
|
||||
compareHosts(foundHostsMap)
|
||||
|
||||
lastDate = time.Now()
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareHosts(foundHostsMap map[string]models.Host) {
|
||||
|
||||
allHosts, ok := gdb.Select("now")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, aHost := range allHosts {
|
||||
|
||||
fHost, exists := foundHostsMap[aHost.Mac]
|
||||
if exists {
|
||||
|
||||
aHost.Iface = fHost.Iface
|
||||
aHost.IP = fHost.IP
|
||||
aHost.Date = fHost.Date
|
||||
aHost.Now = 1
|
||||
|
||||
delete(foundHostsMap, aHost.Mac)
|
||||
|
||||
} else {
|
||||
aHost.Now = 0
|
||||
}
|
||||
gdb.Update("now", aHost)
|
||||
|
||||
aHost.ID = 0
|
||||
aHost.Date = time.Now().Format("2006-01-02 15:04:05")
|
||||
gdb.Update("history", aHost)
|
||||
|
||||
if conf.AppConfig.InfluxEnable {
|
||||
influx.Add(conf.AppConfig, aHost)
|
||||
}
|
||||
if conf.AppConfig.PrometheusEnable {
|
||||
prometheus.Add(aHost)
|
||||
}
|
||||
}
|
||||
|
||||
for _, fHost := range foundHostsMap {
|
||||
|
||||
fHost.Name, fHost.DNS = check.DNS(fHost)
|
||||
notify.Unknown(fHost) // Log and Shoutrrr
|
||||
|
||||
gdb.Update("now", fHost)
|
||||
}
|
||||
}
|
||||
28
backend/internal/routines/trim-history.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package routines
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/gdb"
|
||||
)
|
||||
|
||||
// HistoryTrim - routine for History
|
||||
func HistoryTrim() {
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Duration(1) * time.Hour) // Every hour
|
||||
|
||||
hours := conf.AppConfig.TrimHist
|
||||
nowMinus := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||
date := nowMinus.Format("2006-01-02 15:04:05")
|
||||
|
||||
slog.Info("Removing all History before", "date", date)
|
||||
|
||||
n := gdb.DeleteOldHistory(date)
|
||||
slog.Info("Removed records from History", "n", n)
|
||||
}
|
||||
}()
|
||||
}
|
||||
12
backend/internal/web/index.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func indexHandler(c *gin.Context) {
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", true)
|
||||
}
|
||||
1
backend/internal/web/public/assets/Config.js
Normal file
1
backend/internal/web/public/assets/History.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{t as m,k as n,l as c,h as I,m as $,n as N,p as S,q as E,i as t,e as i,r as M,F as U,b as O,s as _,S as P}from"./index.js";import{M as j}from"./MacHistory.js";var k=m('<input class=form-control placeholder="Show elements"title="Nomber of elements to show"style=max-width:10em;>');function q(l){const o=e=>{localStorage.setItem(l.name,e),n(+e),c()==0&&n(200)};return(()=>{var e=k();return e.$$input=r=>o(r.target.value),e})()}I(["input"]);var A=m('<div class="card border-primary"><div class="card-header d-flex justify-content-between"></div><div class=card-body><table class="table table-striped table-hover"><tbody></tbody>'),D=m("<tr><td class=opacity-50 style=width:2em;>.</td><td><a></a><br><a></a></td><td>");function B(){let l=[];l.push(...$);const o=localStorage.getItem("histShow");return n(+o),(c()===0||isNaN(c()))&&n(200),N(()=>{S()&&(l=[],l.push(...$),console.log("Upd on Filter"),E(!1))}),(()=>{var e=A(),r=e.firstChild,g=r.nextSibling,w=g.firstChild,y=w.firstChild;return t(r,i(M,{}),null),t(r,i(q,{name:"histShow"}),null),t(y,i(P,{get when(){return!S()},get children(){return i(U,{each:l,children:(a,x)=>(()=>{var f=D(),d=f.firstChild,C=d.firstChild,u=d.nextSibling,h=u.firstChild,F=h.nextSibling,b=F.nextSibling,H=u.nextSibling;return t(d,()=>x()+1,C),t(h,()=>a.Name),t(b,()=>a.IP),t(H,i(j,{get mac(){return a.Mac},date:""})),O(s=>{var p="/host/"+a.ID,v="http://"+a.IP;return p!==s.e&&_(h,"href",s.e=p),v!==s.t&&_(b,"href",s.t=v),s},{e:void 0,t:void 0}),f})()})}})),e})()}export{B as default};
|
||||
1
backend/internal/web/public/assets/HostPage.js
Normal 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> <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};
|
||||
4
backend/internal/web/public/assets/MacHistory.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
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};
|
||||
1
backend/internal/web/public/assets/index.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
:root{--transparent-light: #ffffff15}.my-btn{height:100%;background-color:#0000;color:var(--bs-primary);text-align:center;cursor:pointer;padding:1px;border-radius:15%}.my-btn:hover{background-color:var(--transparent-light)}.my-box-on:before,.my-box-off:before{content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='18'></svg>");border-left:thin solid black}.my-box-on{background-color:var(--bs-success)}.my-box-off{background-color:var(--bs-gray-500)}.my-box-on:hover,.my-box-off:hover{background-color:#0000001a}
|
||||
2
backend/internal/web/public/assets/index.js
Normal file
BIN
backend/internal/web/public/favicon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
backend/internal/web/public/version
Normal file
|
|
@ -0,0 +1 @@
|
|||
VERSION=2.1.4
|
||||
19
backend/internal/web/templates/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{{ define "index.html" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<title>WatchYourLAN</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--Favicon-->
|
||||
<link rel="icon" type="image/x-icon" href="/fs/public/favicon.png">
|
||||
<!-- JS & CSS -->
|
||||
<script type="module" crossorigin src="/fs/public/assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/fs/public/assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
66
backend/internal/web/webgui.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/api"
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/conf"
|
||||
"github.com/aceberg/WatchYourLAN/internal/prometheus"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// templFS - html templates
|
||||
//
|
||||
//go:embed templates/*
|
||||
var templFS embed.FS
|
||||
|
||||
// pubFS - public folder
|
||||
//
|
||||
//go:embed public/*
|
||||
var pubFS embed.FS
|
||||
|
||||
// Gui - start web server
|
||||
func Gui() {
|
||||
const (
|
||||
colorCyan = "\033[36m"
|
||||
colorReset = "\033[0m"
|
||||
)
|
||||
|
||||
file, err := pubFS.ReadFile("public/version")
|
||||
check.IfError(err)
|
||||
conf.AppConfig.Version = string(file)[8:]
|
||||
|
||||
address := conf.AppConfig.Host + ":" + conf.AppConfig.Port
|
||||
|
||||
slog.Info(colorCyan + "\n=================================== " +
|
||||
"\n WatchYourLAN Version: " + conf.AppConfig.Version +
|
||||
"\n Config dir: " + conf.AppConfig.DirPath +
|
||||
"\n Default DB: " + conf.AppConfig.UseDB +
|
||||
"\n Log level: " + conf.AppConfig.LogLevel +
|
||||
"\n Web GUI: http://" + address +
|
||||
"\n=================================== " + colorReset)
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
templ := template.Must(template.New("").ParseFS(templFS, "templates/*"))
|
||||
router.SetHTMLTemplate(templ) // templates
|
||||
|
||||
router.StaticFS("/fs/", http.FS(pubFS)) // public
|
||||
|
||||
router.GET("/", indexHandler) // index.go
|
||||
router.GET("/config", indexHandler) // index.go
|
||||
router.GET("/history", indexHandler) // index.go
|
||||
router.GET("/host/*any", indexHandler) // index.go
|
||||
router.GET("/metrics", prometheus.Handler())
|
||||
|
||||
api.Routes(router)
|
||||
|
||||
err = router.Run(address)
|
||||
check.IfError(err)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
PKGDIR=watchyourlan-$1-0_all
|
||||
|
||||
umask 0022
|
||||
|
||||
mkdir -p $PKGDIR/usr/bin
|
||||
mkdir -p $PKGDIR/lib/systemd/system
|
||||
|
||||
cp configs/watchyourlan.service $PKGDIR/lib/systemd/system/
|
||||
|
||||
cp watchyourlan $PKGDIR/usr/bin/
|
||||
|
||||
mkdir -p $PKGDIR/DEBIAN
|
||||
|
||||
echo "Package: watchyourlan
|
||||
Version: $1
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Depends: arp-scan, tzdata
|
||||
Architecture: all
|
||||
Maintainer: aceberg <aceberg_a@proton.me>
|
||||
Description: Lightweight network IP scanner with web GUI
|
||||
" > $PKGDIR/DEBIAN/control
|
||||
|
||||
echo "
|
||||
systemctl daemon-reload
|
||||
" > $PKGDIR/DEBIAN/postinst
|
||||
|
||||
chmod 775 $PKGDIR/DEBIAN/postinst
|
||||
|
||||
dpkg-deb --build --root-owner-group $PKGDIR
|
||||
|
||||
rm -rf $PKGDIR
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd cmd/WatchYourLAN/ && CGO_ENABLED=0 go build -o ../../watchyourlan .
|
||||
cd ../../
|
||||
|
||||
PKGDIR=/opt/watchyourlan
|
||||
|
||||
umask 0022
|
||||
|
||||
mkdir -p $PKGDIR
|
||||
cp watchyourlan $PKGDIR/
|
||||
cp configs/watchyourlan.service $PKGDIR/
|
||||
cp configs/install.sh $PKGDIR/
|
||||
|
||||
cd /opt
|
||||
tar cvzf watchyourlan-$1.tar.gz watchyourlan
|
||||
cd -
|
||||
cp /opt/watchyourlan-$1.tar.gz .
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/check"
|
||||
"github.com/aceberg/WatchYourLAN/internal/web"
|
||||
)
|
||||
|
||||
const configPath = "/data/config.yaml"
|
||||
const nodePath = ""
|
||||
|
||||
func main() {
|
||||
confPtr := flag.String("c", configPath, "Path to config file")
|
||||
nodePtr := flag.String("n", nodePath, "Path to node modules")
|
||||
flag.Parse()
|
||||
|
||||
check.Path(*confPtr)
|
||||
|
||||
web.Gui(*confPtr, *nodePtr) // Start web GUI
|
||||
}
|
||||
37
docker-compose-auth.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
version: "3"
|
||||
services:
|
||||
wyl:
|
||||
image: aceberg/watchyourlan
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ~/.dockerdata/wyl:/data/WatchYourLAN
|
||||
environment:
|
||||
TZ: Asia/Novosibirsk # required: needs your TZ for correct time
|
||||
IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface
|
||||
HOST: "0.0.0.0" # optional, default: 0.0.0.0
|
||||
PORT: "8840" # optional, default: 8840
|
||||
TIMEOUT: "120" # optional, time in seconds, default: 120
|
||||
SHOUTRRR_URL: "" # optional, set url to notify
|
||||
THEME: "sand" # optional
|
||||
COLOR: "dark" # optional
|
||||
|
||||
# WARNING! WYL needs 'host' network mode to work. So, WYL port will be exposed in this setup. You need to limit access to it with firewall or other measures
|
||||
|
||||
forauth:
|
||||
image: aceberg/forauth
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8800:8800 # Proxy port
|
||||
- 8801:8801 # Config port
|
||||
volumes:
|
||||
- ~/.dockerdata/forauth:/data/ForAuth
|
||||
environment:
|
||||
TZ: Asia/Novosibirsk # required: needs your TZ for correct time
|
||||
FA_TARGET: "YOUR_IP:8840" # optional: path to wyl host:port
|
||||
FA_AUTH: "true" # optional: true - enabled, default: false
|
||||
FA_AUTH_EXPIRE: 7d # optional: expiration time, default: 7d
|
||||
FA_AUTH_PASSWORD: "$$2a$$10$$wGLUHXh2cRN1257uGg1s5eZvYgnjw8wB9vAcfcHqqqrxm5hvBqAzK"
|
||||
# WARNING! If password is set as environment variable, every '$' character must be escaped with another '$', like this '$$'
|
||||
# optional: password encrypted with bcrypt, how-to: https://github.com/aceberg/ForAuth/blob/main/docs/BCRYPT.md (In this example FA_AUTH_PASSWORD=pw)
|
||||
FA_AUTH_USER: user # optional: username
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
version: "3"
|
||||
services:
|
||||
node-bootstrap:
|
||||
image: aceberg/node-bootstrap
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8850:8850
|
||||
wyl:
|
||||
image: aceberg/watchyourlan
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
command: "-n http://127.0.0.1:8850"
|
||||
depends_on:
|
||||
- node-bootstrap
|
||||
volumes:
|
||||
- ~/.dockerdata/wyl:/data
|
||||
environment:
|
||||
TZ: Asia/Novosibirsk # required: needs your TZ for correct time
|
||||
IFACE: "enp4s0" # required: 1 or more interface
|
||||
DBPATH: "/data/db.sqlite" # optional, default: /data/db.sqlite
|
||||
GUIIP: "0.0.0.0" # optional, default: localhost
|
||||
GUIPORT: "8840" # optional, default: 8840
|
||||
TIMEOUT: "120" # optional, time in seconds, default: 60
|
||||
SHOUTRRR_URL: "" # optional, set url to notify
|
||||
THEME: "darkly" # optional
|
||||
IGNOREIP: "no" # optional
|
||||
|
|
@ -1,18 +1,28 @@
|
|||
version: "3"
|
||||
services:
|
||||
# node-bootstrap: # optional, local themes and icons
|
||||
# image: aceberg/node-bootstrap # dockerhub
|
||||
# # image: ghcr.io/aceberg/node-bootstrap # or github
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - 8850:8850
|
||||
wyl:
|
||||
image: aceberg/watchyourlan
|
||||
network_mode: "host"
|
||||
image: aceberg/watchyourlan # dockerhub
|
||||
# image: ghcr.io/aceberg/watchyourlan # or github
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
# uncomment those if you are using local node-bootstrap:
|
||||
# command: "-n http://YOUR_IP:8850" # put your server IP or DNS name here
|
||||
# depends_on:
|
||||
# - node-bootstrap
|
||||
volumes:
|
||||
- ~/.dockerdata/wyl:/data
|
||||
- ~/.dockerdata/wyl:/data/WatchYourLAN
|
||||
environment:
|
||||
TZ: Asia/Novosibirsk # required: needs your TZ for correct time
|
||||
IFACE: "virbr-bw wlxf4ec3892dd51" # required: 1 or more interface
|
||||
DBPATH: "/data/db.sqlite" # optional, default: /data/db.sqlite
|
||||
GUIIP: "0.0.0.0" # optional, default: localhost
|
||||
GUIPORT: "8840" # optional, default: 8840
|
||||
TIMEOUT: "120" # optional, time in seconds, default: 60
|
||||
SHOUTRRR_URL: "" # optional, set url to notify
|
||||
THEME: "darkly" # optional
|
||||
IGNOREIP: "no" # optional
|
||||
IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface
|
||||
# HOST: "0.0.0.0" # optional, default: 0.0.0.0
|
||||
# PORT: "8840" # optional, default: 8840
|
||||
# TIMEOUT: "120" # optional, time in seconds, default: 120
|
||||
# SHOUTRRR_URL: "" # optional, set url to notify
|
||||
# THEME: "sand" # optional
|
||||
# COLOR: "dark" # optional
|
||||
64
docs/API.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
## API
|
||||
```http
|
||||
GET /api/all
|
||||
```
|
||||
Returns all hosts in `json`.
|
||||
|
||||
|
||||
```http
|
||||
GET /api/history
|
||||
```
|
||||
Returns all History. Not recommended, the output can be a lot.
|
||||
|
||||
```http
|
||||
GET /api/history/:mac/:date
|
||||
```
|
||||
Returns only history of a device with this `mac` filtered by `date`. `date` format can be anything from `2` to `2025-07` to `2025-07-26`.
|
||||
|
||||
```http
|
||||
GET /api/history/:mac?num=20
|
||||
```
|
||||
Returns only last 20 lines of history of a device with this `mac`.
|
||||
|
||||
|
||||
```http
|
||||
GET /api/host/:id
|
||||
```
|
||||
Returns host with this `id` in `json`.
|
||||
|
||||
|
||||
```http
|
||||
GET /api/port/:addr/:port
|
||||
```
|
||||
Gets state of one `port` of `addr`. Returns `true` if port is open or `false` otherwise.
|
||||
<details>
|
||||
<summary>Request example</summary>
|
||||
|
||||
```bash
|
||||
curl http://0.0.0.0:8840/api/port/192.168.2.2/8844
|
||||
```
|
||||
</details><br>
|
||||
|
||||
|
||||
```http
|
||||
GET /api/edit/:id/:name/*known
|
||||
```
|
||||
Edit host with ID `id`. Can change `name`. `known` is optional, when set to `toggle` will change Known state.
|
||||
|
||||
|
||||
```http
|
||||
GET /api/host/del/:id
|
||||
```
|
||||
Remove host with ID `id`.
|
||||
|
||||
|
||||
```http
|
||||
GET /api/notify_test
|
||||
```
|
||||
Send test notification.
|
||||
|
||||
|
||||
```http
|
||||
GET /api/status/*iface
|
||||
```
|
||||
Show status (Total number of hosts, online/offline, known/unknown). The `iface` parameter is optional and shows status for one interface only. For all interfaces just call `/api/status/`.
|
||||
66
docs/VLAN_ARP_SCAN.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
## Passing arguments to arp-scan
|
||||
|
||||
### 1. IFACES
|
||||
|
||||
`IFACES` is a required variable for WYL to work. It can be set through `GUI`, config file or environment variables.
|
||||
`IFACES` is a list of network interfaces to scan, space separated. For example
|
||||
```sh
|
||||
IFACES: "enp4s0 wlxf4ec3892dd51"
|
||||
```
|
||||
You can get a list of network interfaces by running `ip link show` or `netstat -i`.
|
||||
By default, the scan command will look like this:
|
||||
```sh
|
||||
arp-scan -glNx -I $ONE_IFACE
|
||||
```
|
||||
|
||||
### 2. ARP_ARGS
|
||||
Setting `ARP_ARGS` is optional. It can be set through `GUI`, config file or environment variables.
|
||||
`ARP_ARGS` is additional arguments for `arp-scan`, that will be applied for **every** one of `IFACES`. For example:
|
||||
```sh
|
||||
ARP_ARGS: "-r 1"
|
||||
```
|
||||
```sh
|
||||
arp-scan -glNx -r 1 -I $ONE_IFACE
|
||||
```
|
||||
See `man arp-scan` for all arguments available.
|
||||
|
||||
|
||||
### 3. ARP_STRS for VLANs, docker0 and other complicated scans
|
||||
If `ARP_STRS` is set, it will initiate a completely separate from `IFACES` scan.
|
||||
> [!WARNING]
|
||||
> `ARP_STRS` can be set only through `GUI` or config file. For environment (docker-compose) see `ARP_STRS_JOINED`.
|
||||
|
||||
`ARP_STRS` is a list of strings. `arp-scan` will run for each of them:
|
||||
```sh
|
||||
arp-scan $ONE_STRING
|
||||
```
|
||||
Every string must contain all information you need to pass to `arp-scan`. For example:
|
||||
```sh
|
||||
arp-scan -gNx 10.0.107.0/24 -Q 107 -I eth0
|
||||
```
|
||||
Where `-Q` is a `vlan` id. **Warning:** the last element of string (`eth0` in this example) will be set as `Interface` for found hosts, so it is recommended to put interface at the end.
|
||||
|
||||
|
||||
Setting `ARP_STRS` from config file:
|
||||
```yaml
|
||||
arp_strs:
|
||||
- -gNx 172.17.0.1/24 -I docker0
|
||||
- -glNx -I virbr0
|
||||
```
|
||||
From `GUI` put one string in `Arp Strings` input field, click `Save`, then another empty string will appear.
|
||||
|
||||
### 4. ARP_STRS_JOINED
|
||||
`ARP_STRS_JOINED` is a way to set `ARP_STRS` from ENV. It's a list of strings, comma separated, without spaces before or after comma.
|
||||
```sh
|
||||
ARP_STRS_JOINED: "-gNx 172.17.0.1/24 -I docker0,-gNx 10.0.107.0/24 -Q 107 -I eth0"
|
||||
```
|
||||
|
||||
### 5. Examples
|
||||
vlan id 107
|
||||
```sh
|
||||
ARP_STRS_JOINED: "-gNx 10.0.107.0/24 -Q 107 -I eth0"
|
||||
```
|
||||
docker0
|
||||
```sh
|
||||
ARP_STRS_JOINED: "-gNx 172.17.0.1/24 -I docker0"
|
||||
```
|
||||
24
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
14
frontend/Makefile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
PKG_NAME=WatchYourLAN
|
||||
USR_NAME=aceberg
|
||||
|
||||
build:
|
||||
npm run build && \
|
||||
rm ../backend/internal/web/public/assets/* && \
|
||||
cp -r dist/assets ../backend/internal/web/public
|
||||
|
||||
replace:
|
||||
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
|
||||
|
||||
all: build replace
|
||||
28
frontend/README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
## Usage
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html)
|
||||
13
frontend/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WatchYourLAN</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1664
frontend/package-lock.json
generated
Normal file
21
frontend/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "watchyourlan",
|
||||
"private": true,
|
||||
"version": "2.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solid-primitives/scheduled": "^1.5.2",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"solid-js": "^1.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-solid": "^2.11.2"
|
||||
}
|
||||
}
|
||||
33
frontend/src/App.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* VARS */
|
||||
:root {
|
||||
--transparent-light: #ffffff15;
|
||||
}
|
||||
|
||||
/* Sort button */
|
||||
.my-btn {
|
||||
height: 100%;
|
||||
background-color: #00000000;
|
||||
color: var(--bs-primary);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 1px;
|
||||
border-radius: 15%;
|
||||
}
|
||||
.my-btn:hover {
|
||||
background-color: var(--transparent-light);
|
||||
}
|
||||
|
||||
/* History box */
|
||||
.my-box-on::before, .my-box-off::before {
|
||||
content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='18'></svg>");
|
||||
border-left: thin solid black;
|
||||
}
|
||||
.my-box-on {
|
||||
background-color: var(--bs-success);
|
||||
}
|
||||
.my-box-off {
|
||||
background-color: var(--bs-gray-500);
|
||||
}
|
||||
.my-box-on:hover, .my-box-off:hover {
|
||||
background-color: #0000001a;
|
||||
}
|
||||
38
frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { lazy, onMount } from 'solid-js';
|
||||
import { Router, Route } from "@solidjs/router";
|
||||
import './App.css';
|
||||
import { runAtStart } from './functions/atstart';
|
||||
|
||||
import Body from './pages/Body';
|
||||
import Header from './components/Header';
|
||||
|
||||
function App() {
|
||||
|
||||
onMount(() => {
|
||||
runAtStart();
|
||||
});
|
||||
|
||||
const Config = lazy(() => import("./pages/Config"));
|
||||
const History = lazy(() => import("./pages/History"));
|
||||
const HostPage = lazy(() => import("./pages/HostPage"));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header></Header>
|
||||
<div class="container-lg">
|
||||
<div class="row">
|
||||
<div class="col-md mt-4 mb-4">
|
||||
<Router>
|
||||
<Route path="/" component={Body}/>
|
||||
<Route path="/config" component={Config}/>
|
||||
<Route path="/history" component={History}/>
|
||||
<Route path="/host/:id" component={HostPage}/>
|
||||
</Router>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
50
frontend/src/components/Body/CardHead.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Show } from "solid-js";
|
||||
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() {
|
||||
|
||||
const handleEditNames = (toggle: boolean) => {
|
||||
if (!toggle) {
|
||||
getHosts();
|
||||
}
|
||||
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">
|
||||
<div class="d-flex justify-left">
|
||||
<Filter></Filter>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md mt-1 mb-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<Search></Search>
|
||||
<Show
|
||||
when={editNames()}
|
||||
fallback={<button class="btn btn-outline-primary" title="Toggle edit" onClick={[handleEditNames, true]}>Edit</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardHead
|
||||
45
frontend/src/components/Body/TableHead.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { createSignal, For } from "solid-js";
|
||||
import { Host } from "../../functions/exports";
|
||||
import { sortByAnyField } from "../../functions/sort";
|
||||
|
||||
function TableHead() {
|
||||
|
||||
const [sortField, setSortField] = createSignal<string>('');
|
||||
|
||||
const showSort = () => {
|
||||
let field = localStorage.getItem("sortField") as string;
|
||||
field === "Mac" ? field = "MAC" : '';
|
||||
field === "Hw" ? field = "Hardware" : '';
|
||||
field === "Now" ? field = "On" : '';
|
||||
setSortField(field);
|
||||
};
|
||||
showSort();
|
||||
|
||||
const handleSort = (sortBy: string) => {
|
||||
setSortField(sortBy);
|
||||
sortBy === "MAC" ? sortBy = "Mac" : '';
|
||||
sortBy === "Hardware" ? sortBy = "Hw" : '';
|
||||
sortBy === "On" ? sortBy = "Now" : '';
|
||||
sortByAnyField(sortBy as keyof Host);
|
||||
};
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2em;"></th>
|
||||
<For each={["Name", "Iface", "IP", "MAC", "Hardware", "Date", "Known", "On"]}>{(key) =>
|
||||
<th
|
||||
style={key === sortField() ? "color: var(--bs-primary);" : ''}
|
||||
>{key} <i
|
||||
class="bi bi-sort-down-alt my-btn"
|
||||
onClick={[handleSort, key]}
|
||||
title={"Sort by " + key}
|
||||
></i></th>
|
||||
}</For>
|
||||
<th style="width: 2em;" title="Edit"><i class="bi bi-pencil-fill"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableHead
|
||||
86
frontend/src/components/Body/TableRow.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { createSignal, Show } from "solid-js";
|
||||
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);
|
||||
|
||||
let now = <i class="bi bi-circle-fill" style="color:var(--bs-gray-500);"></i>;
|
||||
if (_props.host.Now == 1) {
|
||||
now = <i class="bi bi-check-circle-fill" style="color:var(--bs-success);"></i>;
|
||||
};
|
||||
|
||||
let known: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);
|
||||
debouncedApi(n);
|
||||
};
|
||||
const handleToggle = async () => {
|
||||
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 (
|
||||
<tr>
|
||||
<td class="opacity-50">{_props.index}.</td>
|
||||
<td>
|
||||
<Show
|
||||
when={editNames()}
|
||||
fallback={name()}
|
||||
>
|
||||
<input type="text" class="form-control" value={name()}
|
||||
onInput={e => handleInput(e.target.value)}></input>
|
||||
</Show>
|
||||
</td>
|
||||
<td>{_props.host.Iface}</td>
|
||||
<td><a href={"http://" + _props.host.IP} target="_blank">{_props.host.IP}</a></td>
|
||||
<td>{_props.host.Mac}</td>
|
||||
<td title={_props.host.Hw}>{_props.host.Hw.slice(0,12)+".."}</td>
|
||||
<td>{_props.host.Date}</td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" checked={known}
|
||||
onClick={handleToggle}></input>
|
||||
</div>
|
||||
</td>
|
||||
<td>{now}</td>
|
||||
<td>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableRow
|
||||
64
frontend/src/components/Config/About.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { createSignal, onMount } from "solid-js";
|
||||
import { apiGetVersion } from "../../functions/api"
|
||||
|
||||
function About() {
|
||||
|
||||
const [version, setVersion] = createSignal('');
|
||||
const [link, setLink] = createSignal('');
|
||||
|
||||
onMount(async () => {
|
||||
const v = await apiGetVersion();
|
||||
setVersion(v);
|
||||
setLink("https://github.com/aceberg/WatchYourLAN/releases/tag/"+v);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">
|
||||
About (<a href={link()} target="_blank">{version()}</a>)
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
83
frontend/src/components/Config/Basic.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { For, Show } from "solid-js";
|
||||
import { apiPath, apiTestNotify } from "../../functions/api"
|
||||
import { appConfig } from "../../functions/exports"
|
||||
|
||||
function Basic() {
|
||||
|
||||
const themes = ["cerulean", "cosmo", "cyborg", "darkly", "emerald", "flatly", "grass", "grayscale", "journal", "litera", "lumen", "lux", "materia", "minty", "morph", "ocean", "pulse", "quartz", "sand", "sandstone", "simplex", "sketchy", "slate", "solar", "spacelab", "superhero", "united", "vapor", "wood", "yeti", "zephyr"];
|
||||
|
||||
const handleTestNotify = () => {
|
||||
apiTestNotify();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">Basic config</div>
|
||||
<div class="card-body table-responsive">
|
||||
<form action={apiPath + '/api/config/'} method="post">
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Host</td>
|
||||
<td><input name="host" type="text" class="form-control" value={appConfig().Host}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port</td>
|
||||
<td><input name="port" type="text" class="form-control" value={appConfig().Port}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Theme</td>
|
||||
<td>
|
||||
<select name="theme" class="form-select">
|
||||
<For each={themes}>{theme =>
|
||||
<Show
|
||||
when={theme == appConfig().Theme}
|
||||
fallback={<option value={theme}>{theme}</option>}
|
||||
>
|
||||
<option value={theme} selected>{theme}</option>
|
||||
</Show>
|
||||
}</For>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Color mode</td>
|
||||
<td>
|
||||
<select name="color" class="form-select">
|
||||
<Show
|
||||
when={appConfig().Color == "dark"}
|
||||
fallback={<>
|
||||
<option value="dark">dark</option>
|
||||
<option value="light" selected>light</option>
|
||||
</>}
|
||||
>
|
||||
<option value="dark" selected>dark</option>
|
||||
<option value="light">light</option>
|
||||
</Show>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local node-bootstrap URL</td>
|
||||
<td><input name="node" type="text" class="form-control" value={appConfig().NodePath}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shoutrrr URL</td>
|
||||
<td>
|
||||
<textarea name="shout" class="form-control" style="width: 100%;" rows="3" wrap="soft">{appConfig().ShoutURL}</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="submit" class="btn btn-primary">Save</button></td>
|
||||
<td><button onClick={handleTestNotify} type="button" style="float: right;" class="btn btn-info">Test notification</button></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Basic
|
||||
17
frontend/src/components/Config/Donate.tsx
Normal 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
|
||||
61
frontend/src/components/Config/Influx.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { apiPath } from "../../functions/api"
|
||||
import { appConfig } from "../../functions/exports"
|
||||
|
||||
function Influx() {
|
||||
|
||||
return (
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">InfluxDB2 config</div>
|
||||
<div class="card-body table-responsive">
|
||||
<form action={apiPath + '/api/config_influx/'} method="post">
|
||||
<table class="table table-borderless"><tbody>
|
||||
<tr>
|
||||
<td>Enable</td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
{appConfig().InfluxEnable
|
||||
? <input class="form-check-input" type="checkbox" name="enable" checked></input>
|
||||
: <input class="form-check-input" type="checkbox" name="enable"></input>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td><input name="addr" type="text" class="form-control" value={appConfig().InfluxAddr}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Token</td>
|
||||
<td><input name="token" type="text" class="form-control" value={appConfig().InfluxToken}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Org</td>
|
||||
<td><input name="org" type="text" class="form-control" value={appConfig().InfluxOrg}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bucket</td>
|
||||
<td><input name="bucket" type="text" class="form-control" value={appConfig().InfluxBucket}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Skip TLS verify</td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
{appConfig().InfluxSkipTLS
|
||||
? <input class="form-check-input" type="checkbox" name="skip" checked></input>
|
||||
: <input class="form-check-input" type="checkbox" name="skip"></input>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="submit" class="btn btn-primary">Save</button></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Influx
|
||||
36
frontend/src/components/Config/Prometheus.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { apiPath } from "../../functions/api"
|
||||
import { appConfig } from "../../functions/exports"
|
||||
|
||||
function Prometheus() {
|
||||
|
||||
return (
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">Prometheus config</div>
|
||||
<div class="card-body table-responsive">
|
||||
<form action={apiPath + '/api/config_prometheus/'} method="post">
|
||||
<table class="table table-borderless"><tbody>
|
||||
<tr>
|
||||
<td>Enable</td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
{appConfig().PrometheusEnable
|
||||
? <input class="form-check-input" type="checkbox" name="enable" checked></input>
|
||||
: <input class="form-check-input" type="checkbox" name="enable"></input>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="submit" class="btn btn-primary">Save</button></td>
|
||||
<td>
|
||||
<a href="/metrics" target="_blank">/metrics</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Prometheus
|
||||
83
frontend/src/components/Config/Scan.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { For, Show } from "solid-js"
|
||||
import { appConfig } from "../../functions/exports"
|
||||
import { apiPath } from "../../functions/api"
|
||||
|
||||
function Scan() {
|
||||
|
||||
return (
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">Scan settings</div>
|
||||
<div class="card-body table-responsive">
|
||||
<form action={apiPath + '/api/config_settings/'} method="post">
|
||||
<table class="table table-borderless"><tbody>
|
||||
<tr>
|
||||
<td>Interfaces</td>
|
||||
<td><input name="ifaces" type="text" class="form-control" value={appConfig().Ifaces}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Timeout (seconds)</td>
|
||||
<td><input name="timeout" type="number" class="form-control" value={appConfig().Timeout}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Args for arp-scan</td>
|
||||
<td><input name="arpargs" type="text" class="form-control" value={appConfig().ArpArgs}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Arp Strings</td>
|
||||
<td>
|
||||
<For each={appConfig().ArpStrs}>{arpStr =>
|
||||
<input name="arpstrs" type="text" class="form-control" value={arpStr}></input>
|
||||
}</For>
|
||||
<input name="arpstrs" type="text" class="form-control"></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log level</td>
|
||||
<td><select name="log" class="form-select">
|
||||
<For each={["debug","info","warn","error"]}>{level =>
|
||||
<Show
|
||||
when={level == appConfig().LogLevel}
|
||||
fallback={<option value={level}>{level}</option>}
|
||||
>
|
||||
<option value={level} selected>{level}</option>
|
||||
</Show>
|
||||
}</For>
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Trim History (hours)</td>
|
||||
<td><input name="trim" type="number" class="form-control" value={appConfig().TrimHist}></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Use DB</td>
|
||||
<td><select name="usedb" class="form-select">
|
||||
<Show
|
||||
when={appConfig().UseDB == "sqlite"}
|
||||
fallback={<>
|
||||
<option value="sqlite">sqlite</option>
|
||||
<option value="postgres" selected>postgres</option>
|
||||
</>}
|
||||
>
|
||||
<option value="sqlite" selected>sqlite</option>
|
||||
<option value="postgres">postgres</option>
|
||||
</Show>
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PG Connect URL</td>
|
||||
<td>
|
||||
<textarea name="pgconnect" class="form-control" style="width: 100%;" rows="3" wrap="soft">{appConfig().PGConnect}</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="submit" class="btn btn-primary">Save</button></td>
|
||||
<td class="text-muted">*Pressing <b>Save</b> button will trigger rescan</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Scan
|
||||
52
frontend/src/components/Filter.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { createSignal, For } from "solid-js";
|
||||
import { Host, ifaces, setHistUpdOnFilter } from "../functions/exports";
|
||||
import { filterFunc } from "../functions/filter";
|
||||
|
||||
|
||||
function Filter() {
|
||||
type FilterEvent = Event & {
|
||||
currentTarget: HTMLSelectElement;
|
||||
target: HTMLSelectElement;
|
||||
};
|
||||
|
||||
const [selectValue, setSelectValue] = createSignal("");
|
||||
|
||||
const handleFilter = (field: keyof Host, event: FilterEvent) => {
|
||||
const value = event.target ? event.target.value : 0;
|
||||
filterFunc(field, value);
|
||||
setHistUpdOnFilter(true);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
filterFunc("ID", 0);
|
||||
setSelectValue("something");
|
||||
setSelectValue("");
|
||||
setHistUpdOnFilter(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="row">
|
||||
<div class="col input-group">
|
||||
<select onChange={(event)=>{handleFilter("Iface", event)}} class="form-select" title="Filter by Iface" value={selectValue()}>
|
||||
<option value="" selected disabled>Iface</option>
|
||||
<For each={ifaces()}>{(iface) =>
|
||||
<option value={iface}>{iface}</option>
|
||||
}</For>
|
||||
</select>
|
||||
<select onChange={(event)=>{handleFilter("Known", event)}} class="form-select" title="Filter by Known" value={selectValue()}>
|
||||
<option value="" selected disabled>Known</option>
|
||||
<option value={1}>Known</option>
|
||||
<option value={0}>Unknown</option>
|
||||
</select>
|
||||
<select onChange={(event)=>{handleFilter("Now", event)}} class="form-select" title="Filter by Online" value={selectValue()}>
|
||||
<option value="" selected disabled>Online</option>
|
||||
<option value={1}>On</option>
|
||||
<option value={0}>Off</option>
|
||||
</select>
|
||||
<button onClick={handleReset} class="btn btn-outline-primary" title="Reset filter">Reset filter</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Filter
|
||||
62
frontend/src/components/Header.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import { appConfig, setAppConfig } from "../functions/exports";
|
||||
import { apiGetConfig } from "../functions/api";
|
||||
|
||||
function Header() {
|
||||
|
||||
const [themePath, setThemePath] = createSignal('');
|
||||
const [iconsPath, setIconsPath] = createSignal('');
|
||||
|
||||
const setCurrentTheme = async () => {
|
||||
setAppConfig(await apiGetConfig());
|
||||
|
||||
const theme = appConfig().Theme?appConfig().Theme:"sand";
|
||||
const color = appConfig().Color?appConfig().Color:"dark";
|
||||
|
||||
if (appConfig().NodePath == '') {
|
||||
setThemePath("https://cdn.jsdelivr.net/npm/aceberg-bootswatch-fork@v5.3.3-2/dist/"+theme+"/bootstrap.min.css");
|
||||
setIconsPath("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css");
|
||||
} else {
|
||||
setThemePath(appConfig().NodePath+"/node_modules/bootswatch/dist/"+theme+"/bootstrap.min.css");
|
||||
setIconsPath(appConfig().NodePath+"/node_modules/bootstrap-icons/font/bootstrap-icons.css");
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute("data-bs-theme", color);
|
||||
color === "dark"
|
||||
? document.documentElement.style.setProperty('--transparent-light', '#ffffff15')
|
||||
: document.documentElement.style.setProperty('--transparent-light', '#00000015');
|
||||
}
|
||||
setCurrentTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="stylesheet" href={iconsPath()}></link> {/* icons */}
|
||||
<link rel="stylesheet" href={themePath()}></link> {/* theme */}
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="/fs/public/favicon.png" style="width: 2em"/>
|
||||
</a>
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/" title="Home">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/config/" title="Config">Config</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/history/" title="History">History</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active fs-3 ms-md-2" target="_blank" href="https://github.com/aceberg/WatchYourLAN" title="Github"><i class="bi bi-github"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default Header
|
||||
17
frontend/src/components/HistShow.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { setShow, show } from "../functions/exports";
|
||||
|
||||
function HistShow(_props: any) {
|
||||
|
||||
const handleSaveShow = (showStr: string) => {
|
||||
localStorage.setItem(_props.name, showStr);
|
||||
|
||||
setShow(+showStr);
|
||||
show() == 0 ? setShow(200) : '';
|
||||
};
|
||||
|
||||
return (
|
||||
<input class="form-control" onInput={e => handleSaveShow(e.target.value)} placeholder="Show elements" title="Nomber of elements to show" style="max-width: 10em;"></input>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistShow
|
||||
42
frontend/src/components/HostPage/HistCard.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { createSignal, onMount } from "solid-js";
|
||||
import { setShow } from "../../functions/exports";
|
||||
import MacHistory from "../MacHistory"
|
||||
|
||||
function HistCard(_props: any) {
|
||||
|
||||
const [today, setToday] = createSignal('');
|
||||
|
||||
onMount(() => {
|
||||
setShow(15000);
|
||||
setToday(new Date().toLocaleDateString("en-CA"));
|
||||
});
|
||||
|
||||
const handleDate = (date: string) => {
|
||||
setToday("");
|
||||
setToday(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<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"
|
||||
value={today()}
|
||||
onInput={(e) => handleDate(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{_props.mac !== "" && today() !== ""
|
||||
? <MacHistory mac={_props.mac} date={today()}></MacHistory>
|
||||
: <>Loading...</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistCard
|
||||
114
frontend/src/components/HostPage/HostCard.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
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;
|
||||
debouncedApi(n);
|
||||
};
|
||||
|
||||
const handleToggle = async () => {
|
||||
|
||||
if (name == "") {
|
||||
name = _props.host.Name;
|
||||
}
|
||||
|
||||
await apiEditHost(_props.host.ID, name, 'toggle');
|
||||
};
|
||||
|
||||
const handleDel = async () => {
|
||||
|
||||
await apiDelHost(_props.host.ID);
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
const handleWOL = async () => {
|
||||
|
||||
await apiWOL(_props.host.Mac);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">Host</div>
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{_props.host.ID}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" value={_props.host.Name}
|
||||
onInput={e => handleInput(e.target.value)}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DNS name</td>
|
||||
<td>{_props.host.DNS}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Iface</td>
|
||||
<td>{_props.host.Iface}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>
|
||||
<a href={"http://" + _props.host.IP} target="_blank">{_props.host.IP}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC</td>
|
||||
<td>{_props.host.Mac}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hardware</td>
|
||||
<td>{_props.host.Hw}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>{_props.host.Date}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Known</td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
onClick={handleToggle}
|
||||
checked={_props.host.Known == 1
|
||||
? true
|
||||
: false
|
||||
}
|
||||
></input>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Online</td>
|
||||
<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>
|
||||
}
|
||||
|
||||
<button type="button" onClick={handleWOL} class="btn btn-outline-success">Wake-on-LAN</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" onClick={handleDel} class="btn btn-outline-danger">Delete host</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HostCard
|
||||