init
This commit is contained in:
42
.dockerignore
Normal file
42
.dockerignore
Normal file
@@ -0,0 +1,42 @@
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Documentation (not needed in container)
|
||||
*.md
|
||||
wiki/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Docker
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
|
||||
# Output files (generated during build)
|
||||
# *.png
|
||||
# *.gv
|
||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
node_modules/
|
||||
# Generated diagram files
|
||||
*.pdf
|
||||
*.gv
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Distribution // packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# -- Build stage --
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Alleen package-bestanden eerst (better caching)
|
||||
COPY package*.json ./
|
||||
RUN npm ci || npm install
|
||||
|
||||
# Dan de rest van de code
|
||||
COPY . .
|
||||
|
||||
# LET OP: pas dit aan als jouw build-script anders heet
|
||||
RUN npm run build
|
||||
|
||||
# -- Serve stage --
|
||||
FROM nginx:alpine
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# Pas dit aan als jouw outputmap niet 'dist' is (bijv. 'build')
|
||||
COPY --from=build /app/dist ./
|
||||
|
||||
# Optioneel: eigen nginx.conf voor SPA routing
|
||||
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
82
README.md
Normal file
82
README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Network Architecture Diagrams
|
||||
|
||||
Python scripts to generate network architecture diagrams using the [Diagrams](https://diagrams.mingrammer.com/) library.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- Graphviz ([installation guide](https://graphviz.org/download/))
|
||||
|
||||
### Installing Graphviz
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
choco install graphviz
|
||||
# or download from https://graphviz.org/download/
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install graphviz
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo apt-get install graphviz # Debian/Ubuntu
|
||||
sudo yum install graphviz # RHEL/CentOS
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create a virtual environment:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
2. Activate the virtual environment:
|
||||
```bash
|
||||
# Windows
|
||||
.venv\Scripts\activate
|
||||
|
||||
# macOS/Linux
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Run the diagram generation scripts:
|
||||
|
||||
```bash
|
||||
# Generate LAN architecture diagram
|
||||
python lan_architecture.py
|
||||
|
||||
# Generate main diagram
|
||||
python main.py
|
||||
```
|
||||
|
||||
Output files will be generated in the current directory as PNG images.
|
||||
|
||||
## Available Diagrams
|
||||
|
||||
- `lan_architecture.py` - Home Lab / Auction Stack Architecture diagram
|
||||
- `main.py` - Network architecture diagram
|
||||
|
||||
## Deployment
|
||||
|
||||
To deploy or share this project:
|
||||
|
||||
1. Ensure Graphviz is installed on the target system
|
||||
2. Clone the repository
|
||||
3. Follow the setup instructions above
|
||||
4. Run the desired diagram script
|
||||
|
||||
## Notes
|
||||
|
||||
- Generated diagram files (`.png`, `.gv`) are excluded from version control
|
||||
- The diagrams use a left-to-right (`LR`) layout direction
|
||||
- Output files are saved with `show=False` to prevent automatic opening
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
nex:
|
||||
build:
|
||||
context: /opt/apps/nex
|
||||
dockerfile: Dockerfile
|
||||
container_name: nex
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.nex.rule=Host(`nex.appmodel.nl`)"
|
||||
- "traefik.http.routers.nex.entrypoints=websecure"
|
||||
- "traefik.http.routers.nex.tls=true"
|
||||
- "traefik.http.services.nex.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.nex-http.rule=Host(`nex.appmodel.nl`)"
|
||||
- "traefik.http.routers.nex-http.entrypoints=web"
|
||||
- "traefik.http.routers.nex-http.middlewares=nex-https"
|
||||
- "traefik.http.middlewares.nex-https.redirectscheme.scheme=https"
|
||||
- "traefik.http.routers.auction.tls.certresolver=letsencrypt",
|
||||
- "traefik.http.middlewares.nex-https.redirectscheme.permanent=true"
|
||||
|
||||
networks:
|
||||
traefik_net:
|
||||
external: true
|
||||
name: traefik_net
|
||||
378
docs/Home.md
Normal file
378
docs/Home.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# Diagram Nex Wiki
|
||||
|
||||
Welcome to the **Diagram Nex** project documentation.
|
||||
|
||||
This project provides a static web interface for viewing and managing network architecture diagrams generated with Python's `diagrams` library.
|
||||
|
||||
## 📚 Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Guide](#development-guide)
|
||||
- [Deployment Guide](#deployment-guide)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- Graphviz
|
||||
- Docker (for deployment)
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone git@git.appmodel.nl:Tour/nex.git
|
||||
cd nex
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Generate diagrams
|
||||
python lan_architecture.py
|
||||
python main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Guide
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
nex/
|
||||
├── Dockerfile # Multi-stage build for production
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── requirements.txt # Python dependencies
|
||||
├── lan_architecture.py # LAN architecture diagram generator
|
||||
├── main.py # Main diagram generator
|
||||
├── public/
|
||||
│ └── index.html # Live Mermaid diagram editor
|
||||
└── wiki/ # Documentation (this wiki)
|
||||
```
|
||||
|
||||
### Adding New Diagrams
|
||||
|
||||
1. Create a new Python file (e.g., `network_diagram.py`)
|
||||
2. Import required components from `diagrams` library
|
||||
3. Define your architecture using context managers
|
||||
4. Run the script to generate PNG output
|
||||
|
||||
Example:
|
||||
```python
|
||||
from diagrams import Diagram, Cluster
|
||||
from diagrams.onprem.network import Router
|
||||
from diagrams.generic.device import Mobile
|
||||
|
||||
with Diagram("My Network", show=False, filename="my_network"):
|
||||
router = Router("Gateway")
|
||||
device = Mobile("Phone")
|
||||
device >> router
|
||||
```
|
||||
|
||||
### Testing Locally
|
||||
|
||||
```bash
|
||||
# Generate diagrams
|
||||
python lan_architecture.py
|
||||
|
||||
# View output
|
||||
ls -la *.png
|
||||
|
||||
# Test with local web server
|
||||
python -m http.server 8000 --directory public/
|
||||
# Open: http://localhost:8000
|
||||
```
|
||||
|
||||
### Available Icons
|
||||
|
||||
The `diagrams` library provides icons from multiple providers:
|
||||
- **OnPrem**: Network, compute, storage, etc.
|
||||
- **Cloud**: AWS, Azure, GCP services
|
||||
- **Generic**: Network devices, blank nodes
|
||||
- **Custom**: Use your own images
|
||||
|
||||
See: [Diagrams Documentation](https://diagrams.mingrammer.com/docs/nodes/overview)
|
||||
|
||||
---
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
### Overview
|
||||
|
||||
The project uses a fully automated Docker-based deployment pipeline:
|
||||
|
||||
1. **Develop** on local machine
|
||||
2. **Commit & Push** to Gitea
|
||||
3. **Auto-Deploy** via post-receive hook
|
||||
4. **Build** multi-stage Docker image
|
||||
5. **Serve** via Traefik reverse proxy
|
||||
|
||||
### Deployment Steps
|
||||
|
||||
#### 1. Initial Setup
|
||||
|
||||
```bash
|
||||
# On server: Create app structure
|
||||
apps-create nex static-fe
|
||||
|
||||
# This creates:
|
||||
# - /opt/apps/nex (git repository)
|
||||
# - /home/tour/infra/nex/docker-compose.yml
|
||||
```
|
||||
|
||||
#### 2. Configure Gitea Hook
|
||||
|
||||
In repository **Settings → Git Hooks → post-receive**:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
/usr/local/bin/app-deploy nex
|
||||
```
|
||||
|
||||
#### 3. Deploy
|
||||
|
||||
Simply push to Gitea:
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
The server automatically:
|
||||
- Pulls latest code
|
||||
- Rebuilds Docker image
|
||||
- Restarts container
|
||||
- Updates live site at https://nex.appmodel.nl
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
If needed:
|
||||
|
||||
```bash
|
||||
# On server
|
||||
app-deploy nex
|
||||
|
||||
# Or step-by-step
|
||||
cd /opt/apps/nex
|
||||
git pull
|
||||
cd /home/tour/infra/nex
|
||||
docker compose up -d --build nex
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# View deployment logs
|
||||
tail -f /var/log/app-deploy-nex.log
|
||||
|
||||
# View container logs
|
||||
cd /home/tour/infra/nex
|
||||
docker compose logs -f nex
|
||||
|
||||
# Check container status
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Infrastructure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Production Architecture │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Gitea │ (Source of Truth) │
|
||||
│ │ Tour/nex │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ git push │
|
||||
│ ↓ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Post-Receive │ (Auto-trigger) │
|
||||
│ │ Hook │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ app-deploy nex │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ /opt/apps/nex/ │ │
|
||||
│ │ (Git Repository) │ │
|
||||
│ └──────┬───────────────┘ │
|
||||
│ │ git pull │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Docker Build Process │ │
|
||||
│ │ ┌─────────────────┐ │ │
|
||||
│ │ │ Stage 1: Build │ │ │
|
||||
│ │ │ - Python │ │ │
|
||||
│ │ │ - Graphviz │ │ │
|
||||
│ │ │ - Generate PNGs │ │ │
|
||||
│ │ └─────────────────┘ │ │
|
||||
│ │ ┌─────────────────┐ │ │
|
||||
│ │ │ Stage 2: Serve │ │ │
|
||||
│ │ │ - Nginx │ │ │
|
||||
│ │ │ - Static files │ │ │
|
||||
│ │ └─────────────────┘ │ │
|
||||
│ └──────┬───────────────┘ │
|
||||
│ │ container starts │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ diagram-nex │ │
|
||||
│ │ Container :80 │ │
|
||||
│ └──────┬───────────────┘ │
|
||||
│ │ traefik_net │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Traefik │ │
|
||||
│ │ Reverse Proxy │ │
|
||||
│ │ + Let's Encrypt │ │
|
||||
│ └──────┬───────────────┘ │
|
||||
│ │ HTTPS │
|
||||
│ ↓ │
|
||||
│ nex.appmodel.nl │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Network Topology
|
||||
|
||||
The diagram nex documents our home lab infrastructure:
|
||||
|
||||
- **LAN 192.168.1.0/24**: Main network
|
||||
- Core server (192.168.1.159): Traefik, Gitea, Dokku, Auction stack, MI50/Ollama
|
||||
- DNS server (192.168.1.163): AdGuard, Artifactory, CI runner
|
||||
- Home Assistant (192.168.1.193)
|
||||
- Atlas worker (192.168.1.100)
|
||||
- IoT devices
|
||||
|
||||
- **Tether 192.168.137.0/24**: Isolated subnet
|
||||
- Hermes worker (192.168.137.239)
|
||||
- Plato worker (192.168.137.163)
|
||||
|
||||
### Services
|
||||
|
||||
| Service | URL | Description |
|
||||
|------------------|-----------------------------|------------------|
|
||||
| Diagram nex | https://nex.appmodel.nl | This application |
|
||||
| Gitea | https://git.appmodel.nl | Git hosting |
|
||||
| Auction Frontend | https://auction.appmodel.nl | Auction platform |
|
||||
| Aupi API | https://aupi.appmodel.nl | Backend API |
|
||||
| Dokku | https://dokku.lan | PaaS platform |
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md) - Detailed deployment guide
|
||||
- [DEPLOYMENT_GUIDE.md](../DEPLOYMENT_GUIDE.md) - General deployment pipeline
|
||||
- [README.md](../README.md) - Project overview
|
||||
|
||||
### External Links
|
||||
|
||||
- [Diagrams Library](https://diagrams.mingrammer.com/)
|
||||
- [Graphviz](https://graphviz.org/)
|
||||
- [Mermaid.js](https://mermaid.js.org/) (used in live editor)
|
||||
- [Traefik Documentation](https://doc.traefik.io/traefik/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/)
|
||||
|
||||
### Contributing
|
||||
|
||||
To contribute to this project:
|
||||
|
||||
1. Create a feature branch
|
||||
2. Make your changes
|
||||
3. Test locally
|
||||
4. Submit a pull request in Gitea
|
||||
5. Wait for automatic deployment after merge
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Diagrams Not Generating
|
||||
|
||||
**Problem**: Python script fails to generate PNG files
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check Graphviz installation
|
||||
dot -V
|
||||
|
||||
# Reinstall if needed
|
||||
# macOS: brew install graphviz
|
||||
# Ubuntu: sudo apt-get install graphviz
|
||||
# Windows: choco install graphviz
|
||||
|
||||
# Verify Python dependencies
|
||||
pip list | grep diagrams
|
||||
pip install --upgrade diagrams
|
||||
```
|
||||
|
||||
#### Container Build Fails
|
||||
|
||||
**Problem**: Docker build fails during deployment
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check deployment logs
|
||||
tail -f /var/log/app-deploy-nex.log
|
||||
|
||||
# Rebuild manually with verbose output
|
||||
cd /home/tour/infra/nex
|
||||
docker compose build --no-cache --progress=plain nex
|
||||
```
|
||||
|
||||
#### Site Not Accessible
|
||||
|
||||
**Problem**: Container runs but site not reachable
|
||||
|
||||
**Solution**:
|
||||
1. Check DNS: `nslookup nex.appmodel.nl`
|
||||
2. Verify Traefik labels: `docker inspect nex-nex | grep traefik`
|
||||
3. Check Traefik logs: `docker logs traefik`
|
||||
4. Test container directly: `curl http://localhost:PORT`
|
||||
|
||||
#### Changes Not Reflected
|
||||
|
||||
**Problem**: Pushed changes but site unchanged
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Force rebuild on server
|
||||
cd /home/tour/infra/nex
|
||||
docker compose down
|
||||
docker compose up -d --build --force-recreate nex
|
||||
|
||||
# Clear browser cache
|
||||
# Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (macOS)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contact & Support
|
||||
|
||||
For issues, questions, or suggestions:
|
||||
|
||||
1. Check this wiki
|
||||
2. Review [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md)
|
||||
3. Check container logs
|
||||
4. Contact the infrastructure team
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-02
|
||||
**Version**: 1.0.0
|
||||
**Maintainer**: Tour
|
||||
31
docs/build-pipeline.md
Normal file
31
docs/build-pipeline.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Appmodel Home Lab – Build & Deploy Pipeline
|
||||
|
||||
Dit document beschrijft hoe een nieuwe applicatie in het home lab wordt aangemaakt en hoe de build- & deploy-pipeline werkt.
|
||||
|
||||
## Overzicht
|
||||
|
||||
- **Broncode**: Gitea (`https://git.appmodel.nl`)
|
||||
- **Build & runtime**: Docker containers op netwerk `traefik_net`
|
||||
- **Routing & TLS**: Traefik (`https://traefik.appmodel.nl`)
|
||||
- **Automatische deploy**: Gitea `post-receive` hooks → `app-deploy <app>`
|
||||
|
||||
## Pipeline in één diagram
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Dev[💻 Dev machine\nVS Code / Git] -->|git push| Gitea[📚 Gitea\nTour/<app>]
|
||||
|
||||
subgraph Server[🏠 Home lab server\n192.168.1.159]
|
||||
Gitea --> Hook[🔔 post-receive hook\n/app-deploy <app>]
|
||||
|
||||
Hook --> Deploy[⚙️ app-deploy <app>\n/git pull + docker compose up -d --build <app>]
|
||||
|
||||
subgraph Docker[🐳 Docker / traefik_net]
|
||||
AppC[🧱 App container\n<app>.appmodel.nl]
|
||||
Traefik[🚦 Traefik\nReverse Proxy]
|
||||
end
|
||||
end
|
||||
|
||||
Deploy --> AppC
|
||||
Traefik --> AppC
|
||||
Client[🌐 Browser / API client] -->|https://<app>.appmodel.nl| Traefik
|
||||
85
docs/deployment.md
Normal file
85
docs/deployment.md
Normal file
@@ -0,0 +1,85 @@
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
%% ============ Internet ============
|
||||
subgraph WAN[🌐 Internet / Cloud]
|
||||
extDNS[(📡 Public DNS)]
|
||||
extGit[(☁️ Externe registries / Git)]
|
||||
end
|
||||
|
||||
%% ============ LAN 192.168.1.x ============
|
||||
subgraph LAN[🏠 LAN 192.168.1.0/24]
|
||||
hub[🛜 Router / Gateway\nhub.lan\n192.168.1.1]
|
||||
|
||||
subgraph core[💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159]
|
||||
traefik[🚦 Traefik\nReverse Proxy]
|
||||
gitea[📚 Gitea\n git.appmodel.nl]
|
||||
dokku[🐳 Dokku\nPaaS / build]
|
||||
auctionFE[🧱 Auction Frontend\n auction.appmodel.nl]
|
||||
aupiAPI[🧱 Auction Backend API\n aupi.appmodel.nl]
|
||||
mi50[🧠 MI50 / Ollama\nAI workloads]
|
||||
end
|
||||
|
||||
subgraph infraDNS[🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163]
|
||||
adguard[🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl]
|
||||
artifactory[📦 Artifactory]
|
||||
runner[⚙️ Build runners]
|
||||
end
|
||||
|
||||
subgraph ha[🏡 Home Automation\nha.lan\n192.168.1.193]
|
||||
hass[🏠 Home Assistant]
|
||||
end
|
||||
|
||||
atlas[🧱 atlas.lan\n192.168.1.100\n]
|
||||
|
||||
iot1[📺 hof-E402NA\n192.168.1.214]
|
||||
iot2[🎧 S380HB\n192.168.1.59]
|
||||
iot3[📟 ecb5faa56c90\n192.168.1.49]
|
||||
iot4[❓ Unknown\n192.168.1.240]
|
||||
end
|
||||
|
||||
%% ============ Tether subnet ============
|
||||
subgraph TETHER[📶 Tether subnet 192.168.137.0/24]
|
||||
hermes[🛰️ hermes.lan\n192.168.137.239\nworker / node]
|
||||
plato[🛰️ plato.lan\n192.168.137.163\nworker / node]
|
||||
end
|
||||
|
||||
%% ============ Verkeer ============
|
||||
|
||||
%% Basis LAN connecties
|
||||
hub --- core
|
||||
hub --- infraDNS
|
||||
hub --- ha
|
||||
hub --- atlas
|
||||
hub --- iot1
|
||||
hub --- iot2
|
||||
hub --- iot3
|
||||
hub --- iot4
|
||||
|
||||
%% WAN koppeling
|
||||
hub --> WAN
|
||||
infraDNS --> WAN
|
||||
|
||||
%% DNS-resolutie
|
||||
core --> adguard
|
||||
ha --> adguard
|
||||
atlas --> adguard
|
||||
TETHER --> adguard
|
||||
|
||||
%% Websites / reverse proxy
|
||||
extDNS --> traefik
|
||||
traefik --> gitea
|
||||
traefik --> auctionFE
|
||||
traefik --> aupiAPI
|
||||
traefik --> dokku
|
||||
|
||||
%% App flow
|
||||
auctionFE --> aupiAPI
|
||||
aupiAPI --> adguard
|
||||
|
||||
%% AI workloads
|
||||
core --> mi50
|
||||
|
||||
%% Tether workers
|
||||
core --- TETHER
|
||||
```
|
||||
94
docs/integration.md
Normal file
94
docs/integration.md
Normal file
@@ -0,0 +1,94 @@
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph P1["PHASE 1: EXTERNAL SCRAPER (Python/Playwright)"]
|
||||
direction LR
|
||||
A1[Listing Pages<br/>/auctions?page=N] --> A2[Extract URLs]
|
||||
B1[Auction Pages<br/>/a/auction-id] --> B2[Parse __NEXT_DATA__ JSON]
|
||||
C1[Lot Pages<br/>/l/lot-id] --> C2[Parse __NEXT_DATA__ JSON]
|
||||
|
||||
A2 --> D1[INSERT auctions to SQLite]
|
||||
B2 --> D1
|
||||
C2 --> D2[INSERT lots & image URLs]
|
||||
|
||||
D1 --> DB[(SQLite Database<br/>output/cache.db)]
|
||||
D2 --> DB
|
||||
end
|
||||
|
||||
DB --> P2_Entry
|
||||
|
||||
subgraph P2["PHASE 2: MONITORING & PROCESSING (Java)"]
|
||||
direction TB
|
||||
P2_Entry[Data Ready] --> Monitor[TroostwijkMonitor<br/>Read lots every hour]
|
||||
P2_Entry --> DBService[DatabaseService<br/>Query & Import]
|
||||
P2_Entry --> Adapter[ScraperDataAdapter<br/>Transform TEXT → INTEGER]
|
||||
|
||||
Monitor --> BM[Bid Monitoring<br/>Check API every 1h]
|
||||
DBService --> IP[Image Processing<br/>Download & Analyze]
|
||||
Adapter --> DataForNotify[Formatted Data]
|
||||
|
||||
BM --> BidUpdate{New bid?}
|
||||
BidUpdate -->|Yes| UpdateDB[Update current_bid in DB]
|
||||
UpdateDB --> NotifyTrigger1
|
||||
|
||||
IP --> Detection[Object Detection<br/>YOLO/OpenCV DNN]
|
||||
Detection --> ObjectCheck{Detect objects?}
|
||||
ObjectCheck -->|Vehicle| Save1[Save labels & estimate value]
|
||||
ObjectCheck -->|Furniture| Save2[Save labels & estimate value]
|
||||
ObjectCheck -->|Machinery| Save3[Save labels & estimate value]
|
||||
Save1 --> NotifyTrigger2
|
||||
Save2 --> NotifyTrigger2
|
||||
Save3 --> NotifyTrigger2
|
||||
|
||||
CA[Closing Alerts<br/>Check < 5 min] --> TimeCheck{Time critical?}
|
||||
TimeCheck -->|Yes| NotifyTrigger3
|
||||
end
|
||||
|
||||
NotifyTrigger1 --> NS
|
||||
NotifyTrigger2 --> NS
|
||||
NotifyTrigger3 --> NS
|
||||
|
||||
subgraph P3["PHASE 3: NOTIFICATION SYSTEM"]
|
||||
NS[NotificationService] --> DN[Desktop Notify<br/>Windows/macOS/Linux]
|
||||
NS --> EN[Email Notify<br/>Gmail SMTP]
|
||||
NS --> PL[Set Priority Level<br/>0=Normal, 1=High]
|
||||
end
|
||||
|
||||
DN --> UI[User Interaction & Decisions]
|
||||
EN --> UI
|
||||
PL --> UI
|
||||
|
||||
subgraph UI_Details[User Decision Points / Trigger Events]
|
||||
direction LR
|
||||
E1["1. BID CHANGE<br/>'Nieuw bod op kavel 12345...'<br/>Actions: Place bid? Monitor? Ignore?"]
|
||||
E2["2. OBJECT DETECTED<br/>'Lot contains: Vehicle...'<br/>Actions: Review? Confirm value?"]
|
||||
E3["3. CLOSING ALERT<br/>'Kavel 12345 sluit binnen 5 min.'<br/>Actions: Place final bid? Let expire?"]
|
||||
E4["4. VIEWING DAY QUESTIONS<br/>'Bezichtiging op [date]...'"]
|
||||
E5["5. ITEM RECOGNITION CONFIRMATION<br/>'Detected: [object]...'"]
|
||||
E6["6. VALUE ESTIMATE APPROVAL<br/>'Geschatte waarde: €X...'"]
|
||||
E7["7. EXCEPTION HANDLING<br/>'Afwijkende sluitingstijd...'"]
|
||||
end
|
||||
|
||||
UI --> UI_Details
|
||||
|
||||
%% Object Detection Sub-Flow Detail
|
||||
subgraph P2_Detail["Object Detection & Value Estimation Pipeline"]
|
||||
direction LR
|
||||
DI[Downloaded Image] --> IPS[ImageProcessingService]
|
||||
IPS --> ODS[ObjectDetectionService]
|
||||
ODS --> Load[Load YOLO model]
|
||||
ODS --> Run[Run inference]
|
||||
ODS --> Post[Post-process detections<br/>confidence > 0.5]
|
||||
Post --> ObjList["Detected Objects List<br/>(80 COCO classes)"]
|
||||
ObjList --> VEL[Value Estimation Logic<br/>Future enhancement]
|
||||
VEL --> Match[Match to categories]
|
||||
VEL --> History[Historical price analysis]
|
||||
VEL --> Condition[Condition assessment]
|
||||
VEL --> Market[Market trends]
|
||||
Market --> ValueEst["Estimated Value Range<br/>Confidence: 75%"]
|
||||
ValueEst --> SaveToDB[Save to Database]
|
||||
SaveToDB --> TriggerNotify{Value > threshold?}
|
||||
end
|
||||
|
||||
IP -.-> P2_Detail
|
||||
TriggerNotify -.-> NotifyTrigger2
|
||||
```
|
||||
282
public/deployment.html
Normal file
282
public/deployment.html
Normal file
@@ -0,0 +1,282 @@
|
||||
<!-- test321 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab diagram – live editor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
|
||||
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
|
||||
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center}
|
||||
header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer}
|
||||
.wrap{display:flex;height:calc(100vh - 40px)}
|
||||
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease}
|
||||
.panel:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
#src{flex:1;padding:.5rem;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:"Consolas","Monaco",monospace;border:none;outline:none;white-space:pre-wrap}
|
||||
#preview{flex:1;padding:.5rem;overflow:auto;background:#fff}
|
||||
/* IMPORTANT: Let the SVG flow naturally and scroll if needed */
|
||||
#preview svg{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||
#error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none}
|
||||
#zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff}
|
||||
/* collapsed state */
|
||||
#srcPanel.collapsed{flex:0}
|
||||
#srcPanel.collapsed #src{display:none}
|
||||
/* Debug: uncomment to see element bounds */
|
||||
/* #preview{outline:2px solid red} #preview svg{outline:2px solid blue} */
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
|
||||
Home-Lab diagram – live Mermaid editor
|
||||
<button onclick="saveSVG()">💾 SVG</button>
|
||||
<button onclick="savePNG()">💾 PNG</button>
|
||||
<button onclick="reset()">🔄 Reset</button>
|
||||
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff">
|
||||
Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="panel" id="srcPanel">
|
||||
<h3>Source (edit here)</h3>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Live preview</h3>
|
||||
<div id="preview"></div>
|
||||
<div id="error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const DEFAULT = `flowchart LR
|
||||
%% ============ Internet ============
|
||||
subgraph WAN[🌐 Internet / Cloud]
|
||||
extDNS[(📡 Public DNS)]
|
||||
extGit[(☁️ Externe registries / Git)]
|
||||
end
|
||||
|
||||
%% ============ LAN 192.168.1.x ============
|
||||
subgraph LAN[🏠 LAN 192.168.1.0/24]
|
||||
hub[🛜 Router / Gateway\\nhub.lan\\n192.168.1.1]
|
||||
|
||||
subgraph core[💻 Hoofdserver / Desktop\\nTour / hephaestus / ollama / dokku.lan\\n192.168.1.159]
|
||||
traefik[🚦 Traefik\\nReverse Proxy]
|
||||
gitea[📚 Gitea\\n git.appmodel.nl]
|
||||
dokku[🐳 Dokku\\nPaaS / build]
|
||||
auctionFE[🧱 Auction Frontend\\n auction.appmodel.nl]
|
||||
aupiAPI[🧱 Auction Backend API\\n aupi.appmodel.nl]
|
||||
mi50[🧠 MI50 / Ollama\\nAI workloads]
|
||||
end
|
||||
|
||||
subgraph infraDNS[🧭 Infra & DNS\\nodroid / dns.lan\\n192.168.1.163]
|
||||
adguard[🧭 AdGuard Home\\nDNS / *.lan / *.appmodel.nl]
|
||||
artifactory[📦 Artifactory]
|
||||
runner[⚙️ Build runners]
|
||||
end
|
||||
|
||||
subgraph ha[🏡 Home Automation\\nha.lan\\n192.168.1.193]
|
||||
hass[🏠 Home Assistant]
|
||||
end
|
||||
|
||||
atlas[🧱 atlas.lan\\n192.168.1.100\\n]
|
||||
|
||||
iot1[📺 hof-E402NA\\n192.168.1.214]
|
||||
iot2[🎧 S380HB\\n192.168.1.59]
|
||||
iot3[📟 ecb5faa56c90\\n192.168.1.49]
|
||||
iot4[❓ Unknown\\n192.168.1.240]
|
||||
end
|
||||
|
||||
%% ============ Tether subnet ============
|
||||
subgraph TETHER[📶 Tether subnet 192.168.137.0/24]
|
||||
hermes[🛰️ hermes.lan\\n192.168.137.239\\nworker / node]
|
||||
plato[🛰️ plato.lan\\n192.168.137.163\\nworker / node]
|
||||
end
|
||||
|
||||
%% ============ Verkeer ============
|
||||
|
||||
%% Basis LAN connecties
|
||||
hub --- core
|
||||
hub --- infraDNS
|
||||
hub --- ha
|
||||
hub --- atlas
|
||||
hub --- iot1
|
||||
hub --- iot2
|
||||
hub --- iot3
|
||||
hub --- iot4
|
||||
|
||||
%% WAN koppeling
|
||||
hub --> WAN
|
||||
infraDNS --> WAN
|
||||
|
||||
%% DNS-resolutie
|
||||
core --> adguard
|
||||
ha --> adguard
|
||||
atlas --> adguard
|
||||
TETHER --> adguard
|
||||
|
||||
%% Websites / reverse proxy
|
||||
extDNS --> traefik
|
||||
traefik --> gitea
|
||||
traefik --> auctionFE
|
||||
traefik --> aupiAPI
|
||||
traefik --> dokku
|
||||
|
||||
%% App flow
|
||||
auctionFE --> aupiAPI
|
||||
aupiAPI --> adguard
|
||||
|
||||
%% AI workloads
|
||||
core --> mi50
|
||||
|
||||
%% Tether workers
|
||||
core --- TETHER
|
||||
`;
|
||||
|
||||
function getStorage(key, fallback) {
|
||||
try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; }
|
||||
}
|
||||
function setStorage(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch {}
|
||||
}
|
||||
|
||||
const srcEl = document.getElementById('src');
|
||||
const errEl = document.getElementById('error');
|
||||
const zoomEl = document.getElementById('zoom');
|
||||
const zoomVal = document.getElementById('zoomVal');
|
||||
const srcPanel = document.getElementById('srcPanel');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
|
||||
srcEl.value = getStorage('labDiagram', DEFAULT);
|
||||
if (getStorage('panelCollapsed', 'false') === 'true') {
|
||||
srcPanel.classList.add('collapsed');
|
||||
toggleBtn.textContent = '🗔 Show source';
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
srcPanel.classList.toggle('collapsed');
|
||||
const collapsed = srcPanel.classList.contains('collapsed');
|
||||
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
|
||||
setStorage('panelCollapsed', collapsed);
|
||||
}
|
||||
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
function render() {
|
||||
const src = srcEl.value;
|
||||
setStorage('labDiagram', src);
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = src;
|
||||
preview.appendChild(mermaidDiv);
|
||||
|
||||
// Render and fix sizing
|
||||
mermaid.init(undefined, mermaidDiv).then(() => {
|
||||
const svg = preview.querySelector('svg');
|
||||
if (svg) {
|
||||
// CRITICAL: Ensure SVG has viewBox for proper sizing
|
||||
if (!svg.hasAttribute('viewBox')) {
|
||||
const {width, height} = svg.getBBox();
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
}
|
||||
svg.style.maxWidth = '100%';
|
||||
svg.style.height = 'auto';
|
||||
applyZoom();
|
||||
}
|
||||
}).catch(e => {
|
||||
errEl.textContent = 'Mermaid error: ' + e.message;
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
srcEl.addEventListener('input', () => {
|
||||
clearTimeout(srcEl._t);
|
||||
srcEl._t = setTimeout(render, 300);
|
||||
});
|
||||
|
||||
zoomEl.addEventListener('input', () => {
|
||||
zoomVal.textContent = zoomEl.value + '%';
|
||||
applyZoom();
|
||||
});
|
||||
|
||||
function applyZoom() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${zoomEl.value / 100})`;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (confirm('Reset to default diagram?')) {
|
||||
srcEl.value = DEFAULT;
|
||||
zoomEl.value = 100;
|
||||
zoomVal.textContent = '100%';
|
||||
setStorage('labDiagram', DEFAULT);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const clone = svg.cloneNode(true);
|
||||
clone.removeAttribute('style');
|
||||
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
|
||||
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'home-lab.svg';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
function savePNG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const {width, height} = svg.getBBox();
|
||||
const canvasW = Math.max(width, 1200);
|
||||
const canvasH = Math.max(height, 800);
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvasW;
|
||||
canvas.height = canvasH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.drawImage(img, 0, 0, canvasW, canvasH);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'home-lab.png';
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
img.onerror = () => alert('PNG conversion failed. Try SVG instead.');
|
||||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- Auto-deployed at $(date122`) -->
|
||||
218
public/dia-kimi.html
Normal file
218
public/dia-kimi.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab nex – Mermaid + Kroki (Graphviz)</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
|
||||
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
|
||||
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-size:1.1rem;font-weight:600}
|
||||
.wrap{display:flex;height:calc(100vh - 40px)}
|
||||
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd}
|
||||
.panel:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
.btn{margin:.2rem .6rem;padding:.3rem .6rem;border:1px solid #ccc;border-radius:4px;background:#fff;cursor:pointer}
|
||||
.btn:hover{background:#eee}
|
||||
#editor{flex:1}
|
||||
.diagram{flex:2;background:#fff;padding:.5rem;overflow:auto}
|
||||
svg{max-width:100%;height:auto}
|
||||
/* mermaid theme tweaks */
|
||||
.mermaid{height:100%;display:flex;align-items:center;justify-content:center}
|
||||
</style>
|
||||
|
||||
<!-- ========= 1. LIBRARIES ========= -->
|
||||
<!-- Monaco editor -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
|
||||
<!-- Mermaid -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
<!-- Kroki client (tiny wrapper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/kroki-client@1/dist/kroki-client.min.js"></script>
|
||||
<!-- html2canvas for PNG export -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>🏠 Home-Lab nex – editable in browser (localStorage auto-saved)</header>
|
||||
|
||||
<div class="wrap">
|
||||
<!-- ---- LEFT: EDITOR ---- -->
|
||||
<div class="panel">
|
||||
<h3>Source (Mermaid syntax)</h3>
|
||||
<div id="editor"></div>
|
||||
<div style="padding:.4rem .6rem">
|
||||
<button class="btn" onclick="render()">▶ Render both</button>
|
||||
<button class="btn" onclick="savePNG()">💾 Save PNG</button>
|
||||
<button class="btn" onclick="saveSVG()">💾 Save SVG</button>
|
||||
<label class="btn">
|
||||
<input type="checkbox" id="dark"> dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ---- MIDDLE: MERMAID ---- -->
|
||||
<div class="panel">
|
||||
<h3>Mermaid render</h3>
|
||||
<div id="mermaid" class="diagram"></div>
|
||||
</div>
|
||||
|
||||
<!-- ---- RIGHT: KROKI / GRAPHVIZ ---- -->
|
||||
<div class="panel">
|
||||
<h3>Kroki (Graphviz) render</h3>
|
||||
<div id="kroki" class="diagram"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ========= 2. DEFAULT SOURCE ========= */
|
||||
const DEFAULT = `flowchart TD
|
||||
%% ---------- colours ----------
|
||||
classDef internet fill:#e1f5ff,stroke:#007bff
|
||||
classDef router fill:#fff3cd,stroke:#ffc107
|
||||
classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px
|
||||
classDef core fill:#ffe6e6,stroke:#dc3545
|
||||
classDef infra fill:#e6ffe6,stroke:#28a745
|
||||
classDef worker fill:#f0e6ff,stroke:#6f42c1
|
||||
classDef iot fill:#fff9e6,stroke:#fd7e14
|
||||
|
||||
%% ---------- nodes ----------
|
||||
internet(🌐 Internet / Cloud):::internet
|
||||
router{{🛜 Router<br/>hub.lan<br/>192.168.1.1}}:::router
|
||||
|
||||
subgraph LAN["🏠 LAN 192.168.1.0/24"]:::lan
|
||||
subgraph CORE["💻 Core server<br/>192.168.1.159"]:::core
|
||||
traefik[🚦 Traefik]:::core
|
||||
gitea[📚 Gitea]:::core
|
||||
dokku[🐳 Dokku]:::core
|
||||
auction[🧱 Auction stack]:::core
|
||||
mi50[🧠 MI50 / Ollama]:::core
|
||||
end
|
||||
|
||||
subgraph INFRA["🧭 Infra & DNS<br/>192.168.1.163"]:::infra
|
||||
adguard[🛡️ AdGuard]:::infra
|
||||
artifactory[📦 Artifactory]:::infra
|
||||
end
|
||||
|
||||
ha[🏡 Home Assistant<br/>192.168.1.193]:::infra
|
||||
atlas[🧱 Atlas<br/>192.168.1.100]:::worker
|
||||
|
||||
iot1[📺 IoT-1]:::iot
|
||||
iot2[📟 IoT-2]:::iot
|
||||
end
|
||||
|
||||
subgraph TETHER["📶 Tether 192.168.137.0/24"]:::lan
|
||||
hermes[🛰️ Hermes]:::worker
|
||||
plato[🛰️ Plato]:::worker
|
||||
end
|
||||
|
||||
dev[👨💻 Dev laptop]:::internet
|
||||
|
||||
%% ---------- edges ----------
|
||||
internet ==> router
|
||||
router --> CORE
|
||||
router --> INFRA
|
||||
router --> ha
|
||||
router --> atlas
|
||||
router --> iot1
|
||||
router --> iot2
|
||||
|
||||
dev ==> gitea
|
||||
dev ==> dokku
|
||||
dev ==> mi50
|
||||
|
||||
traefik --> gitea
|
||||
traefik --> auction
|
||||
traefik --> dokku
|
||||
|
||||
CORE -.->|DNS| adguard
|
||||
ha -.->|DNS| adguard
|
||||
atlas-.->|DNS| adguard
|
||||
hermes-.->|DNS| adguard
|
||||
plato-.->|DNS| adguard
|
||||
|
||||
CORE === TETHER
|
||||
`;
|
||||
|
||||
/* ========= 3. EDITOR SETUP ========= */
|
||||
let editor;
|
||||
require.config({paths:{vs:'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs'}});
|
||||
require(['vs/editor/editor.main'], () => {
|
||||
editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: localStorage.getItem('diagramSrc') || DEFAULT,
|
||||
language: 'markdown',
|
||||
theme: 'vs-dark',
|
||||
minimap: {enabled: false},
|
||||
wordWrap: 'on'
|
||||
});
|
||||
editor.onDidChangeModelContent(() => {
|
||||
localStorage.setItem('diagramSrc', editor.getValue());
|
||||
});
|
||||
render();
|
||||
});
|
||||
|
||||
/* ========= 4. RENDERERS ========= */
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
function render() {
|
||||
const src = editor.getValue();
|
||||
// 4a – mermaid
|
||||
document.getElementById('mermaid').innerHTML = '<div class="mermaid">'+src+'</div>';
|
||||
mermaid.init();
|
||||
// 4b – kroki graphviz
|
||||
Kroki.render('graphviz', dotFromMermaid(src)).then(svg => {
|
||||
document.getElementById('kroki').innerHTML = svg;
|
||||
}).catch(err => {
|
||||
document.getElementById('kroki').innerHTML = '<pre>'+err+'</pre>';
|
||||
});
|
||||
}
|
||||
|
||||
/* crude converter – good enough for this topology */
|
||||
function dotFromMermaid(m) {
|
||||
let dot = 'digraph G {\nbgcolor=transparent;rankdir=TB;node [shape=rect,style=rounded];\n';
|
||||
const lines = m.split('\n');
|
||||
let inSub = false;
|
||||
lines.forEach(l => {
|
||||
l = l.trim();
|
||||
if (l.startsWith('subgraph')) {
|
||||
const name = (l.match(/subgraph\s+(\w+)/) || [, 'cluster'])[1];
|
||||
dot += 'subgraph '+name+' {\nlabel="'+l.split('"')[1]+'";\nstyle=filled;fillcolor=lightgrey;\n';
|
||||
inSub = true;
|
||||
} else if (l === 'end') {
|
||||
dot += '}\n';
|
||||
inSub = false;
|
||||
} else if (l.includes('[') && l.includes(']')) {
|
||||
const id = l.split('[')[0].trim();
|
||||
const label = (l.match(/\[(.*?)\]/) || [, id])[1];
|
||||
dot += id + ' [label="' + label.replace(/:::.*$/,'') + '"];\n';
|
||||
} else if (l.includes('-->') || l.includes('==>') || l.includes('-.->')) {
|
||||
const arr = l.replace(/[~=>\-.]+/g,'->').split('->').map(x=>x.trim()).filter(Boolean);
|
||||
if (arr.length === 2) dot += arr[0] + ' -> ' + arr[1] + ';\n';
|
||||
}
|
||||
});
|
||||
dot += '}';
|
||||
return dot;
|
||||
}
|
||||
|
||||
/* ========= 5. EXPORT ========= */
|
||||
function savePNG() {
|
||||
html2canvas(document.querySelector('#mermaid svg')).then(canvas => {
|
||||
download(canvas.toDataURL(), 'home-lab.png');
|
||||
});
|
||||
}
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#mermaid svg');
|
||||
const url = 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg.outerHTML);
|
||||
download(url, 'home-lab.svg');
|
||||
}
|
||||
function download(href, name) {
|
||||
const a = Object.assign(document.createElement('a'), {href, download: name});
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
}
|
||||
|
||||
/* dark toggle */
|
||||
document.getElementById('dark').onchange = e => {
|
||||
document.body.style.background = e.target.checked ? '#121212' : '#f7f9fc';
|
||||
document.body.style.color = e.target.checked ? '#eee' : '#222';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
107
public/dia.html
Normal file
107
public/dia.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Netwerk architectuur</title>
|
||||
<!-- Mermaid via CDN -->
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({ startOnLoad: true, theme: "default" });
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; }
|
||||
.mermaid { max-width: 100%; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Netwerk architectuur</h1>
|
||||
|
||||
<div class="mermaid">
|
||||
flowchart LR
|
||||
|
||||
%% ============ Internet ============
|
||||
subgraph WAN[🌐 Internet / Cloud]
|
||||
extDNS[(📡 Public DNS)]
|
||||
extGit[(☁️ Externe registries / Git)]
|
||||
end
|
||||
|
||||
%% ============ LAN 192.168.1.x ============
|
||||
subgraph LAN[🏠 LAN 192.168.1.0/24]
|
||||
hub[🛜 Router / Gateway\nhub.lan\n192.168.1.1]
|
||||
|
||||
subgraph core[💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159]
|
||||
traefik[🚦 Traefik\nReverse Proxy]
|
||||
gitea[📚 Gitea\n git.appmodel.nl]
|
||||
dokku[🐳 Dokku\nPaaS / build]
|
||||
auctionFE[🧱 Auction Frontend\n auction.appmodel.nl]
|
||||
aupiAPI[🧱 Auction Backend API\n aupi.appmodel.nl]
|
||||
mi50[🧠 MI50 / Ollama\nAI workloads]
|
||||
end
|
||||
|
||||
subgraph infraDNS[🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163]
|
||||
adguard[🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl]
|
||||
artifactory[📦 Artifactory]
|
||||
runner[⚙️ Build runners]
|
||||
end
|
||||
|
||||
subgraph ha[🏡 Home Automation\nha.lan\n192.168.1.193]
|
||||
hass[🏠 Home Assistant]
|
||||
end
|
||||
|
||||
atlas[🧱 atlas.lan\n192.168.1.100\n]
|
||||
|
||||
iot1[📺 hof-E402NA\n192.168.1.214]
|
||||
iot2[🎧 S380HB\n192.168.1.59]
|
||||
iot3[📟 ecb5faa56c90\n192.168.1.49]
|
||||
iot4[❓ Unknown\n192.168.1.240]
|
||||
end
|
||||
|
||||
%% ============ Tether subnet ============
|
||||
subgraph TETHER[📶 Tether subnet 192.168.137.0/24]
|
||||
hermes[🛰️ hermes.lan\n192.168.137.239\nworker / node]
|
||||
plato[🛰️ plato.lan\n192.168.137.163\nworker / node]
|
||||
end
|
||||
|
||||
%% ============ Verkeer ============
|
||||
|
||||
%% Basis LAN connecties
|
||||
hub --- core
|
||||
hub --- infraDNS
|
||||
hub --- ha
|
||||
hub --- atlas
|
||||
hub --- iot1
|
||||
hub --- iot2
|
||||
hub --- iot3
|
||||
hub --- iot4
|
||||
|
||||
%% WAN koppeling
|
||||
hub --> WAN
|
||||
infraDNS --> WAN
|
||||
|
||||
%% DNS-resolutie
|
||||
core --> adguard
|
||||
ha --> adguard
|
||||
atlas --> adguard
|
||||
TETHER --> adguard
|
||||
|
||||
%% Websites / reverse proxy
|
||||
extDNS --> traefik
|
||||
traefik --> gitea
|
||||
traefik --> auctionFE
|
||||
traefik --> aupiAPI
|
||||
traefik --> dokku
|
||||
|
||||
%% App flow
|
||||
auctionFE --> aupiAPI
|
||||
aupiAPI --> adguard
|
||||
|
||||
%% AI workloads
|
||||
core --> mi50
|
||||
|
||||
%% Tether workers
|
||||
core --- TETHER
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
7
public/img/favicon.svg
Normal file
7
public/img/favicon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#2563eb" rx="15"/>
|
||||
<path d="M25 40 L50 20 L75 40 L75 70 L25 70 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2"/>
|
||||
<circle cx="50" cy="45" r="8" fill="#2563eb"/>
|
||||
<rect x="40" y="55" width="20" height="3" fill="#2563eb"/>
|
||||
<text x="50" y="90" font-family="Arial" font-size="12" fill="#ffffff" text-anchor="middle" font-weight="bold">AUCTION</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 465 B |
BIN
public/img/home.png
Normal file
BIN
public/img/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
BIN
public/img/home_lab_architecture.png
Normal file
BIN
public/img/home_lab_architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
BIN
public/img/img.png
Normal file
BIN
public/img/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
public/img/network-architecture.gv.png
Normal file
BIN
public/img/network-architecture.gv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
266
public/index.html
Normal file
266
public/index.html
Normal file
@@ -0,0 +1,266 @@
|
||||
<!-- test321 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab diagram – live editor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
|
||||
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
|
||||
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center}
|
||||
header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer}
|
||||
.wrap{display:flex;height:calc(100vh - 40px)}
|
||||
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease}
|
||||
.panel:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
#src{flex:1;padding:.5rem;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:"Consolas","Monaco",monospace;border:none;outline:none;white-space:pre-wrap}
|
||||
#preview{flex:1;padding:.5rem;overflow:auto;background:#fff}
|
||||
/* IMPORTANT: Let the SVG flow naturally and scroll if needed */
|
||||
#preview svg{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||
#error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none}
|
||||
#zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff}
|
||||
/* collapsed state */
|
||||
#srcPanel.collapsed{flex:0}
|
||||
#srcPanel.collapsed #src{display:none}
|
||||
/* Debug: uncomment to see element bounds */
|
||||
/* #preview{outline:2px solid red} #preview svg{outline:2px solid blue} */
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
|
||||
Home-Lab diagram – live Mermaid editor
|
||||
<button onclick="saveSVG()">💾 SVG</button>
|
||||
<button onclick="savePNG()">💾 PNG</button>
|
||||
<button onclick="reset()">🔄 Reset</button>
|
||||
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff">
|
||||
Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="panel" id="srcPanel">
|
||||
<h3>Source (edit here)</h3>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Live preview</h3>
|
||||
<div id="preview"></div>
|
||||
<div id="error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const DEFAULT = `flowchart TD
|
||||
%% ---------- styles ----------
|
||||
classDef internet fill:#e1f5ff,stroke:#007bff
|
||||
classDef router fill:#fff3cd,stroke:#ffc107
|
||||
classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px
|
||||
classDef core fill:#ffe6e6,stroke:#dc3545
|
||||
classDef infra fill:#e6ffe6,stroke:#28a745
|
||||
classDef worker fill:#f0e6ff,stroke:#6f42c1
|
||||
classDef iot fill:#fff9e6,stroke:#fd7e14
|
||||
|
||||
%% ---------- nodes ----------
|
||||
internet(🌐 Internet / Cloud):::internet
|
||||
router[🛜 Router\nhub.lan\n192.168.1.1]:::router
|
||||
|
||||
subgraph LAN [🏠 LAN 192.168.1.0/24]
|
||||
subgraph CORE [💻 Core server\n192.168.1.159]
|
||||
traefik[🚦 Traefik]:::core
|
||||
gitea[📚 Gitea]:::core
|
||||
dokku[🐳 Dokku]:::core
|
||||
auction[🧱 Auction stack]:::core
|
||||
mi50[🧠 MI50 / Ollama]:::core
|
||||
end
|
||||
|
||||
subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
|
||||
adguard[🛡️ AdGuard]:::infra
|
||||
artifactory[📦 Artifactory]:::infra
|
||||
end
|
||||
|
||||
ha[🏡 Home Assistant\n192.168.1.193]:::infra
|
||||
atlas[🧱 Atlas\n192.168.1.100]:::worker
|
||||
|
||||
iot1[📺 IoT-1]:::iot
|
||||
iot2[📟 IoT-2]:::iot
|
||||
end
|
||||
|
||||
subgraph TETHER [📶 Tether 192.168.137.0/24]
|
||||
hermes[🛰️ Hermes]:::worker
|
||||
plato[🛰️ Plato]:::worker
|
||||
end
|
||||
|
||||
dev[👨💻 Dev laptop]:::internet
|
||||
|
||||
%% ---------- edges ----------
|
||||
internet ==> router
|
||||
router --> CORE
|
||||
router --> INFRA
|
||||
router --> ha
|
||||
router --> atlas
|
||||
router --> iot1
|
||||
router --> iot2
|
||||
|
||||
dev ==> gitea
|
||||
dev ==> dokku
|
||||
dev ==> mi50
|
||||
|
||||
traefik --> gitea
|
||||
traefik --> auction
|
||||
traefik --> dokku
|
||||
|
||||
CORE -.->|DNS| adguard
|
||||
ha -.->|DNS| adguard
|
||||
atlas-.->|DNS| adguard
|
||||
hermes-.->|DNS| adguard
|
||||
plato-.->|DNS| adguard
|
||||
|
||||
CORE === TETHER
|
||||
`;
|
||||
|
||||
function getStorage(key, fallback) {
|
||||
try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; }
|
||||
}
|
||||
function setStorage(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch {}
|
||||
}
|
||||
|
||||
const srcEl = document.getElementById('src');
|
||||
const errEl = document.getElementById('error');
|
||||
const zoomEl = document.getElementById('zoom');
|
||||
const zoomVal = document.getElementById('zoomVal');
|
||||
const srcPanel = document.getElementById('srcPanel');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
|
||||
srcEl.value = getStorage('labDiagram', DEFAULT);
|
||||
if (getStorage('panelCollapsed', 'false') === 'true') {
|
||||
srcPanel.classList.add('collapsed');
|
||||
toggleBtn.textContent = '🗔 Show source';
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
srcPanel.classList.toggle('collapsed');
|
||||
const collapsed = srcPanel.classList.contains('collapsed');
|
||||
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
|
||||
setStorage('panelCollapsed', collapsed);
|
||||
}
|
||||
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
function render() {
|
||||
const src = srcEl.value;
|
||||
setStorage('labDiagram', src);
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = src;
|
||||
preview.appendChild(mermaidDiv);
|
||||
|
||||
// Render and fix sizing
|
||||
mermaid.init(undefined, mermaidDiv).then(() => {
|
||||
const svg = preview.querySelector('svg');
|
||||
if (svg) {
|
||||
// CRITICAL: Ensure SVG has viewBox for proper sizing
|
||||
if (!svg.hasAttribute('viewBox')) {
|
||||
const {width, height} = svg.getBBox();
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
}
|
||||
svg.style.maxWidth = '100%';
|
||||
svg.style.height = 'auto';
|
||||
applyZoom();
|
||||
}
|
||||
}).catch(e => {
|
||||
errEl.textContent = 'Mermaid error: ' + e.message;
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
srcEl.addEventListener('input', () => {
|
||||
clearTimeout(srcEl._t);
|
||||
srcEl._t = setTimeout(render, 300);
|
||||
});
|
||||
|
||||
zoomEl.addEventListener('input', () => {
|
||||
zoomVal.textContent = zoomEl.value + '%';
|
||||
applyZoom();
|
||||
});
|
||||
|
||||
function applyZoom() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${zoomEl.value / 100})`;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (confirm('Reset to default diagram?')) {
|
||||
srcEl.value = DEFAULT;
|
||||
zoomEl.value = 100;
|
||||
zoomVal.textContent = '100%';
|
||||
setStorage('labDiagram', DEFAULT);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const clone = svg.cloneNode(true);
|
||||
clone.removeAttribute('style');
|
||||
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
|
||||
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'home-lab.svg';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
function savePNG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const {width, height} = svg.getBBox();
|
||||
const canvasW = Math.max(width, 1200);
|
||||
const canvasH = Math.max(height, 800);
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvasW;
|
||||
canvas.height = canvasH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.drawImage(img, 0, 0, canvasW, canvasH);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'home-lab.png';
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
img.onerror = () => alert('PNG conversion failed. Try SVG instead.');
|
||||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- Auto-deployed at $(date122`) -->
|
||||
283
public/preview.html
Normal file
283
public/preview.html
Normal file
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab diagram – live editor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
|
||||
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
|
||||
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center}
|
||||
header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer}
|
||||
.wrap{display:flex;height:calc(100vh - 40px)}
|
||||
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease}
|
||||
.panel:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
#src, #preview{flex:1;padding:.5rem;overflow:auto;background:#fff;font-family:"Consolas","Monaco",monospace}
|
||||
#src{border:none;outline:none;background:#1e1e1e;color:#d4d4d4;white-space:pre-wrap}
|
||||
#preview{display:flex;align-items:flex-start;justify-content:center;min-height:0}
|
||||
svg{max-width:100%;height:auto}
|
||||
#error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none}
|
||||
#zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff}
|
||||
/* collapsed state */
|
||||
#srcPanel.collapsed{flex:0}
|
||||
#srcPanel.collapsed #src{display:none}
|
||||
</style>
|
||||
<!-- ONLY Mermaid – NO Monaco, NO AMD loader -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
|
||||
Home-Lab diagram – live Mermaid editor
|
||||
<button onclick="saveSVG()">💾 SVG</button>
|
||||
<button onclick="savePNG()">💾 PNG</button>
|
||||
<button onclick="reset()">🔄 Reset</button>
|
||||
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff">
|
||||
Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="panel" id="srcPanel">
|
||||
<h3>Source (edit here)</h3>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Live preview</h3>
|
||||
<div id="preview"></div>
|
||||
<div id="error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ---------- DEFAULT SOURCE – guaranteed valid Mermaid 10.6.1 ---------- */
|
||||
const DEFAULT = `flowchart TD
|
||||
%% ---------- styles ----------
|
||||
classDef internet fill:#e1f5ff,stroke:#007bff
|
||||
classDef router fill:#fff3cd,stroke:#ffc107
|
||||
classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px
|
||||
classDef core fill:#ffe6e6,stroke:#dc3545
|
||||
classDef infra fill:#e6ffe6,stroke:#28a745
|
||||
classDef worker fill:#f0e6ff,stroke:#6f42c1
|
||||
classDef iot fill:#fff9e6,stroke:#fd7e14
|
||||
|
||||
%% ---------- nodes ----------
|
||||
internet(🌐 Internet / Cloud):::internet
|
||||
router[🛜 Router\nhub.lan\n192.168.1.1]:::router
|
||||
|
||||
subgraph LAN [🏠 LAN 192.168.1.0/24]
|
||||
subgraph CORE [💻 Core server\n192.168.1.159]
|
||||
traefik[🚦 Traefik]:::core
|
||||
gitea[📚 Gitea]:::core
|
||||
dokku[🐳 Dokku]:::core
|
||||
auction[🧱 Auction stack]:::core
|
||||
mi50[🧠 MI50 / Ollama]:::core
|
||||
end
|
||||
|
||||
subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
|
||||
adguard[🛡️ AdGuard]:::infra
|
||||
artifactory[📦 Artifactory]:::infra
|
||||
end
|
||||
|
||||
ha[🏡 Home Assistant\n192.168.1.193]:::infra
|
||||
atlas[🧱 Atlas\n192.168.1.100]:::worker
|
||||
|
||||
iot1[📺 IoT-1]:::iot
|
||||
iot2[📟 IoT-2]:::iot
|
||||
end
|
||||
|
||||
subgraph TETHER [📶 Tether 192.168.137.0/24]
|
||||
hermes[🛰️ Hermes]:::worker
|
||||
plato[🛰️ Plato]:::worker
|
||||
end
|
||||
|
||||
dev[👨💻 Dev laptop]:::internet
|
||||
|
||||
%% ---------- edges ----------
|
||||
internet ==> router
|
||||
router --> CORE
|
||||
router --> INFRA
|
||||
router --> ha
|
||||
router --> atlas
|
||||
router --> iot1
|
||||
router --> iot2
|
||||
|
||||
dev ==> gitea
|
||||
dev ==> dokku
|
||||
dev ==> mi50
|
||||
|
||||
traefik --> gitea
|
||||
traefik --> auction
|
||||
traefik --> dokku
|
||||
|
||||
CORE -.->|DNS| adguard
|
||||
ha -.->|DNS| adguard
|
||||
atlas-.->|DNS| adguard
|
||||
hermes-.->|DNS| adguard
|
||||
plato-.->|DNS| adguard
|
||||
|
||||
CORE === TETHER
|
||||
`;
|
||||
|
||||
/* ---------- safe localStorage access ---------- */
|
||||
function getStorage(key, fallback) {
|
||||
try {
|
||||
const val = localStorage.getItem(key);
|
||||
return val !== null ? val : fallback;
|
||||
} catch (e) {
|
||||
// IntelliJ preview blocks localStorage
|
||||
console.warn('localStorage unavailable:', e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
function setStorage(key, val) {
|
||||
try {
|
||||
localStorage.setItem(key, val);
|
||||
} catch (e) {
|
||||
console.warn('localStorage save failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- load state ---------- */
|
||||
const srcEl = document.getElementById('src');
|
||||
const errEl = document.getElementById('error');
|
||||
srcEl.value = getStorage('labDiagram', DEFAULT);
|
||||
|
||||
const srcPanel = document.getElementById('srcPanel');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
if (getStorage('panelCollapsed', 'false') === 'true') {
|
||||
srcPanel.classList.add('collapsed');
|
||||
toggleBtn.textContent = '🗔 Show source';
|
||||
}
|
||||
|
||||
/* ---------- panel toggle ---------- */
|
||||
function togglePanel() {
|
||||
srcPanel.classList.toggle('collapsed');
|
||||
const collapsed = srcPanel.classList.contains('collapsed');
|
||||
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
|
||||
setStorage('panelCollapsed', collapsed);
|
||||
}
|
||||
|
||||
/* ---------- Mermaid init ---------- */
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
/* ---------- render ---------- */
|
||||
function render() {
|
||||
const src = srcEl.value;
|
||||
setStorage('labDiagram', src);
|
||||
showError('');
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = src;
|
||||
preview.appendChild(mermaidDiv);
|
||||
|
||||
// Render then zoom
|
||||
mermaid.init(undefined, mermaidDiv).then(() => {
|
||||
const svg = preview.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.maxWidth = 'none';
|
||||
svg.style.width = 'auto';
|
||||
svg.style.height = 'auto';
|
||||
svg.style.transformOrigin = 'top left';
|
||||
applyZoom();
|
||||
}
|
||||
}).catch(e => {
|
||||
showError('Mermaid error: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errEl.textContent = msg;
|
||||
errEl.style.display = msg ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/* ---------- auto-render + zoom ---------- */
|
||||
srcEl.addEventListener('input', () => {
|
||||
clearTimeout(srcEl._t);
|
||||
srcEl._t = setTimeout(render, 300);
|
||||
});
|
||||
|
||||
const zoomEl = document.getElementById('zoom');
|
||||
zoomEl.addEventListener('input', () => {
|
||||
document.getElementById('zoomVal').textContent = zoomEl.value + '%';
|
||||
applyZoom();
|
||||
});
|
||||
|
||||
function applyZoom() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${zoomEl.value / 100})`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- actions ---------- */
|
||||
function reset() {
|
||||
if (confirm('Reset to default diagram?')) {
|
||||
srcEl.value = DEFAULT;
|
||||
zoomEl.value = 100;
|
||||
document.getElementById('zoomVal').textContent = '100%';
|
||||
setStorage('labDiagram', DEFAULT);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const clone = svg.cloneNode(true);
|
||||
clone.removeAttribute('style');
|
||||
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
|
||||
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'home-lab.svg';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
function savePNG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const bbox = svg.getBBox();
|
||||
const width = Math.max(bbox.width, 1200);
|
||||
const height = Math.max(bbox.height, 800);
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'home-lab.png';
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
img.onerror = () => alert('PNG conversion failed. Try SVG instead.');
|
||||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
diagrams==0.25.1
|
||||
graphviz==0.20.3
|
||||
780
resources/network_discovery_20251207_160645.json
Normal file
780
resources/network_discovery_20251207_160645.json
Normal file
@@ -0,0 +1,780 @@
|
||||
{
|
||||
"discovery_time": "2025-12-07T16:06:45.539721",
|
||||
"total_devices": 76,
|
||||
"networks_scanned": [
|
||||
"192.168.1.0/24",
|
||||
"192.168.137.1/24",
|
||||
"172.20.240.1/20",
|
||||
"192.168.137.0/24",
|
||||
"192.168.1.100/24",
|
||||
"192.168.137.100/24"
|
||||
],
|
||||
"devices": [
|
||||
{
|
||||
"ip": "192.168.1.1",
|
||||
"hostname": "192.168.1.1",
|
||||
"mac": "b8:d5:26:d2:7d:c0",
|
||||
"open_ports": [
|
||||
5000
|
||||
],
|
||||
"type": "router",
|
||||
"icon": "\ud83d\udedc",
|
||||
"last_seen": "2025-12-07T15:52:09.705400"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.10",
|
||||
"hostname": "192.168.1.10",
|
||||
"mac": "40:9f:38:1c:1f:01",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:52:09.705400"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.49",
|
||||
"hostname": "192.168.1.49",
|
||||
"mac": "ec:b5:fa:a5:6c:90",
|
||||
"open_ports": [
|
||||
443,
|
||||
80,
|
||||
8080
|
||||
],
|
||||
"type": "server",
|
||||
"icon": "\ud83d\udcbb",
|
||||
"last_seen": "2025-12-07T15:52:22.062446"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.44",
|
||||
"hostname": "192.168.1.44",
|
||||
"mac": "48:27:e2:d4:d9:54",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:52:22.157183"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.59",
|
||||
"hostname": "192.168.1.59",
|
||||
"mac": "04:17:b6:82:05:1a",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:52:24.079670"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.65",
|
||||
"hostname": "192.168.1.65",
|
||||
"mac": "ea:e7:a7:88:43:e1",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:52:34.995741"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.100",
|
||||
"hostname": "host.docker.internal",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:52:36.610550"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.102",
|
||||
"hostname": "192.168.1.102",
|
||||
"mac": "cc:f4:11:cd:76:50",
|
||||
"open_ports": [
|
||||
8008,
|
||||
8443
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:52:47.145897"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.136",
|
||||
"hostname": "192.168.1.136",
|
||||
"mac": "b0:4a:39:58:ed:6a",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:52:58.676214"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.159",
|
||||
"hostname": "git.appmodel.nl",
|
||||
"mac": "50:eb:f6:2a:98:30",
|
||||
"open_ports": [
|
||||
80,
|
||||
8080,
|
||||
443,
|
||||
22,
|
||||
3000
|
||||
],
|
||||
"type": "server",
|
||||
"icon": "\ud83d\udcbb",
|
||||
"last_seen": "2025-12-07T15:53:00.598585"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.151",
|
||||
"hostname": "192.168.1.151",
|
||||
"mac": "34:cd:b0:2b:d9:a0",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:53:10.286185"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.163",
|
||||
"hostname": "192.168.1.163",
|
||||
"mac": "00:1e:06:31:b1:27",
|
||||
"open_ports": [
|
||||
22,
|
||||
8000,
|
||||
8080
|
||||
],
|
||||
"type": "dns",
|
||||
"icon": "\ud83d\udee1\ufe0f",
|
||||
"last_seen": "2025-12-07T15:53:31.910265"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.165",
|
||||
"hostname": "HP678531.home",
|
||||
"mac": "48:ba:4e:67:85:32",
|
||||
"open_ports": [
|
||||
80,
|
||||
443,
|
||||
8080
|
||||
],
|
||||
"type": "server",
|
||||
"icon": "\ud83d\udcbb",
|
||||
"last_seen": "2025-12-07T15:53:32.065413"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.193",
|
||||
"hostname": "homeassistant",
|
||||
"mac": "e4:b9:7a:9f:3b:5f",
|
||||
"open_ports": [],
|
||||
"type": "home_automation",
|
||||
"icon": "\ud83c\udfe1",
|
||||
"last_seen": "2025-12-07T15:53:54.643647"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.206",
|
||||
"hostname": "192.168.1.206",
|
||||
"mac": "7c:df:a1:54:d4:fc",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:54:16.332734"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.214",
|
||||
"hostname": "192.168.1.214",
|
||||
"mac": "10:7b:44:30:a0:b9",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:54:17.164333"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.240",
|
||||
"hostname": "192.168.1.240",
|
||||
"mac": "4c:49:29:7d:bc:e4",
|
||||
"open_ports": [
|
||||
8443,
|
||||
8008
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:54:39.613611"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.243",
|
||||
"hostname": "192.168.1.243",
|
||||
"mac": "ac:67:84:61:90:d9",
|
||||
"open_ports": [
|
||||
8443,
|
||||
8008
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:55:01.218760"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.1",
|
||||
"hostname": "MI",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:55:02.284598"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.100",
|
||||
"hostname": "MI",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:55:11.113935"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.239",
|
||||
"hostname": "192.168.137.239",
|
||||
"mac": "18:31:bf:4e:d7:8a",
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:55:41.136168"
|
||||
},
|
||||
{
|
||||
"ip": "172.20.240.1",
|
||||
"hostname": "MI.mshome.net",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:55:46.119130"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.1",
|
||||
"hostname": "MI",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:55:49.126499"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.3",
|
||||
"hostname": "192.168.137.3",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:56:09.203344"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.13",
|
||||
"hostname": "192.168.137.13",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:56:10.560591"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.35",
|
||||
"hostname": "192.168.137.35",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:56:32.787426"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.61",
|
||||
"hostname": "192.168.137.61",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:56:55.788256"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.72",
|
||||
"hostname": "192.168.137.72",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:56:56.768989"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.100",
|
||||
"hostname": "MI",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:56:58.613419"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.102",
|
||||
"hostname": "192.168.137.102",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:57:19.952047"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.114",
|
||||
"hostname": "192.168.137.114",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:57:20.802691"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.137",
|
||||
"hostname": "192.168.137.137",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:57:42.904414"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.161",
|
||||
"hostname": "192.168.137.161",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:58:05.741701"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.172",
|
||||
"hostname": "192.168.137.172",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:58:06.758378"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.203",
|
||||
"hostname": "192.168.137.203",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:58:29.845884"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.217",
|
||||
"hostname": "192.168.137.217",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:58:30.827941"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.239",
|
||||
"hostname": "hermes.lan",
|
||||
"mac": "18:31:bf:4e:d7:8a",
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:58:48.627302"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.244",
|
||||
"hostname": "192.168.137.244",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T15:59:09.825882"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.10",
|
||||
"hostname": "192.168.1.10",
|
||||
"mac": "40:9f:38:1c:1f:01",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:59:31.412126"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.1",
|
||||
"hostname": "192.168.1.1",
|
||||
"mac": "b8:d5:26:d2:7d:c0",
|
||||
"open_ports": [
|
||||
5000
|
||||
],
|
||||
"type": "router",
|
||||
"icon": "\ud83d\udedc",
|
||||
"last_seen": "2025-12-07T15:59:31.412126"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.49",
|
||||
"hostname": "192.168.1.49",
|
||||
"mac": "ec:b5:fa:a5:6c:90",
|
||||
"open_ports": [
|
||||
8080,
|
||||
80,
|
||||
443
|
||||
],
|
||||
"type": "server",
|
||||
"icon": "\ud83d\udcbb",
|
||||
"last_seen": "2025-12-07T15:59:54.631041"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.44",
|
||||
"hostname": "192.168.1.44",
|
||||
"mac": "48:27:e2:d4:d9:54",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:59:54.769952"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.42",
|
||||
"hostname": "192.168.1.42",
|
||||
"mac": "38:2c:e5:45:be:3c",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:59:54.884149"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.59",
|
||||
"hostname": "192.168.1.59",
|
||||
"mac": "04:17:b6:82:05:1a",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T15:59:56.630244"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.65",
|
||||
"hostname": "192.168.1.65",
|
||||
"mac": "ea:e7:a7:88:43:e1",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:00:18.468155"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.100",
|
||||
"hostname": "host.docker.internal",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:00:20.098760"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.93",
|
||||
"hostname": "192.168.1.93",
|
||||
"mac": "fe:67:72:c1:c9:8c",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:00:41.565323"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.102",
|
||||
"hostname": "192.168.1.102",
|
||||
"mac": "cc:f4:11:cd:76:50",
|
||||
"open_ports": [
|
||||
8443,
|
||||
8008
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:01:03.168820"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.136",
|
||||
"hostname": "192.168.1.136",
|
||||
"mac": "b0:4a:39:58:ed:6a",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:01:25.683522"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.159",
|
||||
"hostname": "git.appmodel.nl",
|
||||
"mac": "50:eb:f6:2a:98:30",
|
||||
"open_ports": [
|
||||
80,
|
||||
443,
|
||||
3000,
|
||||
8080,
|
||||
22
|
||||
],
|
||||
"type": "server",
|
||||
"icon": "\ud83d\udcbb",
|
||||
"last_seen": "2025-12-07T16:01:27.603014"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.151",
|
||||
"hostname": "192.168.1.151",
|
||||
"mac": "34:cd:b0:2b:d9:a0",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:01:48.219580"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.163",
|
||||
"hostname": "192.168.1.163",
|
||||
"mac": "00:1e:06:31:b1:27",
|
||||
"open_ports": [
|
||||
22,
|
||||
8080,
|
||||
8000
|
||||
],
|
||||
"type": "dns",
|
||||
"icon": "\ud83d\udee1\ufe0f",
|
||||
"last_seen": "2025-12-07T16:02:09.802235"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.161",
|
||||
"hostname": "192.168.1.161",
|
||||
"mac": "4c:ba:d7:a6:c1:8c",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:02:09.802870"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.165",
|
||||
"hostname": "HP678531.home",
|
||||
"mac": "48:ba:4e:67:85:32",
|
||||
"open_ports": [
|
||||
443,
|
||||
80,
|
||||
8080
|
||||
],
|
||||
"type": "server",
|
||||
"icon": "\ud83d\udcbb",
|
||||
"last_seen": "2025-12-07T16:02:09.817569"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.190",
|
||||
"hostname": "192.168.1.190",
|
||||
"mac": "c8:c9:a3:60:0f:9d",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:02:31.453384"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.193",
|
||||
"hostname": "homeassistant",
|
||||
"mac": "e4:b9:7a:9f:3b:5f",
|
||||
"open_ports": [],
|
||||
"type": "home_automation",
|
||||
"icon": "\ud83c\udfe1",
|
||||
"last_seen": "2025-12-07T16:02:32.140000"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.206",
|
||||
"hostname": "192.168.1.206",
|
||||
"mac": "7c:df:a1:54:d4:fc",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:02:53.865089"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.214",
|
||||
"hostname": "192.168.1.214",
|
||||
"mac": "10:7b:44:30:a0:b9",
|
||||
"open_ports": [],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:02:54.661758"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.240",
|
||||
"hostname": "192.168.1.240",
|
||||
"mac": "4c:49:29:7d:bc:e4",
|
||||
"open_ports": [
|
||||
8443,
|
||||
8008
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:03:17.131184"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.243",
|
||||
"hostname": "192.168.1.243",
|
||||
"mac": "ac:67:84:61:90:d9",
|
||||
"open_ports": [
|
||||
8443,
|
||||
8008
|
||||
],
|
||||
"type": "unknown",
|
||||
"icon": "\u2753",
|
||||
"last_seen": "2025-12-07T16:03:38.896155"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.1",
|
||||
"hostname": "MI",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:03:39.964337"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.5",
|
||||
"hostname": "192.168.137.5",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:00.058709"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.11",
|
||||
"hostname": "192.168.137.11",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:00.737328"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.46",
|
||||
"hostname": "192.168.137.46",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:23.792363"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.53",
|
||||
"hostname": "192.168.137.53",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:24.722390"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.100",
|
||||
"hostname": "MI",
|
||||
"mac": null,
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:29.581053"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.82",
|
||||
"hostname": "192.168.137.82",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:47.713917"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.91",
|
||||
"hostname": "192.168.137.91",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:04:49.066341"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.116",
|
||||
"hostname": "192.168.137.116",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:05:11.425890"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.142",
|
||||
"hostname": "192.168.137.142",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:05:34.246370"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.156",
|
||||
"hostname": "192.168.137.156",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:05:35.568531"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.174",
|
||||
"hostname": "192.168.137.174",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:05:57.866182"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.206",
|
||||
"hostname": "192.168.137.206",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:06:20.912642"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.215",
|
||||
"hostname": "192.168.137.215",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:06:21.781321"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.239",
|
||||
"hostname": "hermes.lan",
|
||||
"mac": "18:31:bf:4e:d7:8a",
|
||||
"open_ports": [
|
||||
22
|
||||
],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:06:39.618322"
|
||||
},
|
||||
{
|
||||
"ip": "192.168.137.233",
|
||||
"hostname": "192.168.137.233",
|
||||
"mac": null,
|
||||
"open_ports": [],
|
||||
"type": "worker",
|
||||
"icon": "\ud83d\udef0\ufe0f",
|
||||
"last_seen": "2025-12-07T16:06:43.724109"
|
||||
}
|
||||
]
|
||||
}
|
||||
1326
resources/network_discovery_20251207_163512.json
Normal file
1326
resources/network_discovery_20251207_163512.json
Normal file
File diff suppressed because it is too large
Load Diff
9
resources/network_summary_20251207_160645.txt
Normal file
9
resources/network_summary_20251207_160645.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
============================================================
|
||||
NETWORK DISCOVERY SUMMARY
|
||||
============================================================
|
||||
|
||||
Discovery Time: 2025-12-07 16:06:45
|
||||
Total Devices Found: 76
|
||||
|
||||
Devices by Type:
|
||||
----------------------------------------
|
||||
9
resources/network_summary_20251207_163512.txt
Normal file
9
resources/network_summary_20251207_163512.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
============================================================
|
||||
NETWORK DISCOVERY SUMMARY
|
||||
============================================================
|
||||
|
||||
Discovery Time: 2025-12-07 16:35:12
|
||||
Total Devices Found: 96
|
||||
|
||||
Devices by Type:
|
||||
----------------------------------------
|
||||
172
resources/network_topology.txt
Normal file
172
resources/network_topology.txt
Normal file
@@ -0,0 +1,172 @@
|
||||
flowchart TD
|
||||
%% ---------- styles ----------
|
||||
classDef internet fill:#e1f5ff,stroke:#007bff
|
||||
classDef router fill:#fff3cd,stroke:#ffc107
|
||||
classDef server fill:#ffe6e6,stroke:#dc3545
|
||||
classDef dns fill:#e6ffe6,stroke:#28a745
|
||||
classDef homeauto fill:#fff0f5,stroke:#e83e8c
|
||||
classDef worker fill:#f0e6ff,stroke:#6f42c1
|
||||
classDef iot fill:#fff9e6,stroke:#fd7e14
|
||||
classDef unknown fill:#f8f9fa,stroke:#6c757d
|
||||
|
||||
subgraph LAN["LAN 192.168.1.0/24 - 40 devices"]
|
||||
subgraph CORE["Core Infrastructure"]
|
||||
192_168_1_1["[Router] 192.168.1.1<br/>192.168.1.1"]
|
||||
class 192_168_1_1 router;
|
||||
192_168_1_49["[Server] 192.168.1.49<br/>192.168.1.49"]
|
||||
class 192_168_1_49 server;
|
||||
192_168_1_100["[Worker] host.docker.internal<br/>192.168.1.100"]
|
||||
class 192_168_1_100 worker;
|
||||
192_168_1_159["[Server] git.appmodel.nl<br/>192.168.1.159"]
|
||||
class 192_168_1_159 server;
|
||||
192_168_1_163["[DNS] 192.168.1.163<br/>192.168.1.163"]
|
||||
class 192_168_1_163 dns;
|
||||
192_168_1_165["[Server] HP678531.home<br/>192.168.1.165"]
|
||||
class 192_168_1_165 server;
|
||||
192_168_1_193["[Home] homeassistant<br/>192.168.1.193"]
|
||||
class 192_168_1_193 homeauto;
|
||||
192_168_1_1["[Router] 192.168.1.1<br/>192.168.1.1"]
|
||||
class 192_168_1_1 router;
|
||||
192_168_1_49["[Server] 192.168.1.49<br/>192.168.1.49"]
|
||||
class 192_168_1_49 server;
|
||||
192_168_1_100["[Worker] host.docker.internal<br/>192.168.1.100"]
|
||||
class 192_168_1_100 worker;
|
||||
192_168_1_159["[Server] git.appmodel.nl<br/>192.168.1.159"]
|
||||
class 192_168_1_159 server;
|
||||
192_168_1_163["[DNS] 192.168.1.163<br/>192.168.1.163"]
|
||||
class 192_168_1_163 dns;
|
||||
192_168_1_165["[Server] HP678531.home<br/>192.168.1.165"]
|
||||
class 192_168_1_165 server;
|
||||
192_168_1_193["[Home] homeassistant<br/>192.168.1.193"]
|
||||
class 192_168_1_193 homeauto;
|
||||
end
|
||||
192_168_1_10["[?] 192.168.1.10<br/>192.168.1.10"]
|
||||
class 192_168_1_10 unknown;
|
||||
192_168_1_44["[?] 192.168.1.44<br/>192.168.1.44"]
|
||||
class 192_168_1_44 unknown;
|
||||
192_168_1_59["[?] 192.168.1.59<br/>192.168.1.59"]
|
||||
class 192_168_1_59 unknown;
|
||||
192_168_1_65["[?] 192.168.1.65<br/>192.168.1.65"]
|
||||
class 192_168_1_65 unknown;
|
||||
192_168_1_102["[?] 192.168.1.102<br/>192.168.1.102"]
|
||||
class 192_168_1_102 unknown;
|
||||
192_168_1_136["[?] 192.168.1.136<br/>192.168.1.136"]
|
||||
class 192_168_1_136 unknown;
|
||||
192_168_1_151["[?] 192.168.1.151<br/>192.168.1.151"]
|
||||
class 192_168_1_151 unknown;
|
||||
192_168_1_206["[?] 192.168.1.206<br/>192.168.1.206"]
|
||||
class 192_168_1_206 unknown;
|
||||
192_168_1_214["[?] 192.168.1.214<br/>192.168.1.214"]
|
||||
class 192_168_1_214 unknown;
|
||||
192_168_1_240["[?] 192.168.1.240<br/>192.168.1.240"]
|
||||
class 192_168_1_240 unknown;
|
||||
192_168_1_243["[?] 192.168.1.243<br/>192.168.1.243"]
|
||||
class 192_168_1_243 unknown;
|
||||
192_168_1_10["[?] 192.168.1.10<br/>192.168.1.10"]
|
||||
class 192_168_1_10 unknown;
|
||||
192_168_1_44["[?] 192.168.1.44<br/>192.168.1.44"]
|
||||
class 192_168_1_44 unknown;
|
||||
192_168_1_42["[?] 192.168.1.42<br/>192.168.1.42"]
|
||||
class 192_168_1_42 unknown;
|
||||
192_168_1_59["[?] 192.168.1.59<br/>192.168.1.59"]
|
||||
class 192_168_1_59 unknown;
|
||||
192_168_1_65["[?] 192.168.1.65<br/>192.168.1.65"]
|
||||
class 192_168_1_65 unknown;
|
||||
192_168_1_93["[?] 192.168.1.93<br/>192.168.1.93"]
|
||||
class 192_168_1_93 unknown;
|
||||
192_168_1_102["[?] 192.168.1.102<br/>192.168.1.102"]
|
||||
class 192_168_1_102 unknown;
|
||||
192_168_1_136["[?] 192.168.1.136<br/>192.168.1.136"]
|
||||
class 192_168_1_136 unknown;
|
||||
192_168_1_151["[?] 192.168.1.151<br/>192.168.1.151"]
|
||||
class 192_168_1_151 unknown;
|
||||
192_168_1_161["[?] 192.168.1.161<br/>192.168.1.161"]
|
||||
class 192_168_1_161 unknown;
|
||||
192_168_1_190["[?] 192.168.1.190<br/>192.168.1.190"]
|
||||
class 192_168_1_190 unknown;
|
||||
192_168_1_206["[?] 192.168.1.206<br/>192.168.1.206"]
|
||||
class 192_168_1_206 unknown;
|
||||
192_168_1_214["[?] 192.168.1.214<br/>192.168.1.214"]
|
||||
class 192_168_1_214 unknown;
|
||||
192_168_1_240["[?] 192.168.1.240<br/>192.168.1.240"]
|
||||
class 192_168_1_240 unknown;
|
||||
192_168_1_243["[?] 192.168.1.243<br/>192.168.1.243"]
|
||||
class 192_168_1_243 unknown;
|
||||
end
|
||||
|
||||
subgraph TETHER["Tether 192.168.137.0/24 - 35 devices"]
|
||||
192_168_137_1["[Worker] MI<br/>192.168.137.1"]
|
||||
class 192_168_137_1 worker;
|
||||
192_168_137_100["[Worker] MI<br/>192.168.137.100"]
|
||||
class 192_168_137_100 worker;
|
||||
192_168_137_239["[Hermes] 192.168.137.239<br/>192.168.137.239"]
|
||||
class 192_168_137_239 worker;
|
||||
192_168_137_1["[Worker] MI<br/>192.168.137.1"]
|
||||
class 192_168_137_1 worker;
|
||||
192_168_137_3["[Worker] 192.168.137.3<br/>192.168.137.3"]
|
||||
class 192_168_137_3 worker;
|
||||
192_168_137_13["[Worker] 192.168.137.13<br/>192.168.137.13"]
|
||||
class 192_168_137_13 worker;
|
||||
192_168_137_35["[Worker] 192.168.137.35<br/>192.168.137.35"]
|
||||
class 192_168_137_35 worker;
|
||||
192_168_137_61["[Worker] 192.168.137.61<br/>192.168.137.61"]
|
||||
class 192_168_137_61 worker;
|
||||
192_168_137_72["[Worker] 192.168.137.72<br/>192.168.137.72"]
|
||||
class 192_168_137_72 worker;
|
||||
192_168_137_100["[Worker] MI<br/>192.168.137.100"]
|
||||
class 192_168_137_100 worker;
|
||||
192_168_137_102["[Worker] 192.168.137.102<br/>192.168.137.102"]
|
||||
class 192_168_137_102 worker;
|
||||
192_168_137_114["[Worker] 192.168.137.114<br/>192.168.137.114"]
|
||||
class 192_168_137_114 worker;
|
||||
192_168_137_137["[Worker] 192.168.137.137<br/>192.168.137.137"]
|
||||
class 192_168_137_137 worker;
|
||||
192_168_137_161["[Worker] 192.168.137.161<br/>192.168.137.161"]
|
||||
class 192_168_137_161 worker;
|
||||
192_168_137_172["[Worker] 192.168.137.172<br/>192.168.137.172"]
|
||||
class 192_168_137_172 worker;
|
||||
192_168_137_203["[Worker] 192.168.137.203<br/>192.168.137.203"]
|
||||
class 192_168_137_203 worker;
|
||||
192_168_137_217["[Worker] 192.168.137.217<br/>192.168.137.217"]
|
||||
class 192_168_137_217 worker;
|
||||
192_168_137_239["[Hermes] hermes.lan<br/>192.168.137.239"]
|
||||
class 192_168_137_239 worker;
|
||||
192_168_137_244["[Worker] 192.168.137.244<br/>192.168.137.244"]
|
||||
class 192_168_137_244 worker;
|
||||
192_168_137_1["[Worker] MI<br/>192.168.137.1"]
|
||||
class 192_168_137_1 worker;
|
||||
192_168_137_5["[Worker] 192.168.137.5<br/>192.168.137.5"]
|
||||
class 192_168_137_5 worker;
|
||||
192_168_137_11["[Worker] 192.168.137.11<br/>192.168.137.11"]
|
||||
class 192_168_137_11 worker;
|
||||
192_168_137_46["[Worker] 192.168.137.46<br/>192.168.137.46"]
|
||||
class 192_168_137_46 worker;
|
||||
192_168_137_53["[Worker] 192.168.137.53<br/>192.168.137.53"]
|
||||
class 192_168_137_53 worker;
|
||||
192_168_137_100["[Worker] MI<br/>192.168.137.100"]
|
||||
class 192_168_137_100 worker;
|
||||
192_168_137_82["[Worker] 192.168.137.82<br/>192.168.137.82"]
|
||||
class 192_168_137_82 worker;
|
||||
192_168_137_91["[Worker] 192.168.137.91<br/>192.168.137.91"]
|
||||
class 192_168_137_91 worker;
|
||||
192_168_137_116["[Worker] 192.168.137.116<br/>192.168.137.116"]
|
||||
class 192_168_137_116 worker;
|
||||
192_168_137_142["[Worker] 192.168.137.142<br/>192.168.137.142"]
|
||||
class 192_168_137_142 worker;
|
||||
192_168_137_156["[Worker] 192.168.137.156<br/>192.168.137.156"]
|
||||
class 192_168_137_156 worker;
|
||||
192_168_137_174["[Worker] 192.168.137.174<br/>192.168.137.174"]
|
||||
class 192_168_137_174 worker;
|
||||
192_168_137_206["[Worker] 192.168.137.206<br/>192.168.137.206"]
|
||||
class 192_168_137_206 worker;
|
||||
192_168_137_215["[Worker] 192.168.137.215<br/>192.168.137.215"]
|
||||
class 192_168_137_215 worker;
|
||||
192_168_137_239["[Hermes] hermes.lan<br/>192.168.137.239"]
|
||||
class 192_168_137_239 worker;
|
||||
192_168_137_233["[Worker] 192.168.137.233<br/>192.168.137.233"]
|
||||
class 192_168_137_233 worker;
|
||||
end
|
||||
|
||||
%% ---------- Connections ----------
|
||||
192_168_1_159 -.-> TETHER
|
||||
192_168_1_1 --> LAN
|
||||
625
src/discovery.py
Normal file
625
src/discovery.py
Normal file
@@ -0,0 +1,625 @@
|
||||
import socket
|
||||
import subprocess
|
||||
import json
|
||||
import ipaddress
|
||||
from datetime import datetime
|
||||
import re
|
||||
import concurrent.futures
|
||||
import ctypes
|
||||
|
||||
|
||||
class WindowsNetworkDiscoverer:
|
||||
def __init__(self):
|
||||
self.icon_mapping = {
|
||||
'router': '🛜',
|
||||
'server': '💻',
|
||||
'nas': '🧱',
|
||||
'docker': '🐳',
|
||||
'git': '📚',
|
||||
'reverse_proxy': '🚦',
|
||||
'ai': '🧠',
|
||||
'dns': '🛡️',
|
||||
'registry': '📦',
|
||||
'home_automation': '🏡',
|
||||
'worker': '🛰️',
|
||||
'iot': '📺',
|
||||
'laptop': '👨💻',
|
||||
'internet': '🌐',
|
||||
'unknown': '❓'
|
||||
}
|
||||
|
||||
def get_local_networks(self):
|
||||
"""Get local networks using Windows ipconfig"""
|
||||
networks = []
|
||||
|
||||
try:
|
||||
# Run ipconfig and parse output
|
||||
result = subprocess.run(['ipconfig', '/all'],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='ignore')
|
||||
|
||||
current_ip = None
|
||||
current_mask = None
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
# Look for IPv4 Address
|
||||
if 'IPv4 Address' in line and '(Preferred)' in line:
|
||||
match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
|
||||
if match:
|
||||
current_ip = match.group(1)
|
||||
|
||||
# Look for Subnet Mask
|
||||
elif 'Subnet Mask' in line and current_ip:
|
||||
match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
|
||||
if match:
|
||||
current_mask = match.group(1)
|
||||
|
||||
# Convert to CIDR
|
||||
if current_ip and current_mask and not current_ip.startswith('169.254.'):
|
||||
# Calculate CIDR from subnet mask
|
||||
mask_parts = current_mask.split('.')
|
||||
cidr = sum(bin(int(x)).count('1') for x in mask_parts)
|
||||
network = f"{current_ip}/{cidr}"
|
||||
networks.append(network)
|
||||
|
||||
current_ip = None
|
||||
current_mask = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting networks: {e}")
|
||||
|
||||
# Add your specific networks from the diagram
|
||||
if '192.168.1.0/24' not in networks:
|
||||
networks.append('192.168.1.0/24')
|
||||
if '192.168.137.0/24' not in networks:
|
||||
networks.append('192.168.137.0/24')
|
||||
|
||||
return list(set(networks))
|
||||
|
||||
def windows_ping(self, ip):
|
||||
"""Windows-compatible ping check"""
|
||||
try:
|
||||
# Windows ping command
|
||||
result = subprocess.run(
|
||||
['ping', '-n', '1', '-w', '1000', ip],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
|
||||
# Check for successful ping in output
|
||||
return ('Reply from' in result.stdout) or ('bytes=' in result.stdout)
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def get_windows_mac(self, ip):
|
||||
"""Get MAC address using Windows arp command"""
|
||||
try:
|
||||
# First ensure the IP is in ARP cache by pinging
|
||||
subprocess.run(['ping', '-n', '1', '-w', '500', ip],
|
||||
capture_output=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
|
||||
# Get ARP entry
|
||||
result = subprocess.run(['arp', '-a', ip],
|
||||
capture_output=True, text=True)
|
||||
|
||||
# Parse ARP output - Windows format
|
||||
for line in result.stdout.split('\n'):
|
||||
if ip in line:
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
# MAC address format: xx-xx-xx-xx-xx-xx or xx:xx:xx:xx:xx:xx
|
||||
if '-' in part or ':' in part:
|
||||
# Standardize to colon format
|
||||
mac = part.replace('-', ':')
|
||||
if len(mac) == 17: # Valid MAC length
|
||||
return mac
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def scan_ports(self, ip, ports):
|
||||
"""Scan common ports on a host"""
|
||||
open_ports = []
|
||||
|
||||
def check_port(port):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex((ip, port))
|
||||
sock.close()
|
||||
return port if result == 0 else None
|
||||
except:
|
||||
return None
|
||||
|
||||
# Scan in parallel
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = [executor.submit(check_port, port) for port in ports]
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
open_ports.append(result)
|
||||
|
||||
return open_ports
|
||||
|
||||
def identify_device(self, ip, hostname, open_ports):
|
||||
"""Identify device type based on IP, hostname, and open ports"""
|
||||
hostname_lower = hostname.lower()
|
||||
|
||||
# Known devices from your diagram
|
||||
if ip == '192.168.1.1':
|
||||
return 'router', '🛜'
|
||||
elif ip == '192.168.1.159':
|
||||
return 'server', '💻'
|
||||
elif ip == '192.168.1.163':
|
||||
return 'dns', '🛡️'
|
||||
elif ip == '192.168.1.193':
|
||||
return 'home_automation', '🏡'
|
||||
elif ip == '192.168.1.100':
|
||||
return 'worker', '🛰️'
|
||||
elif ip.startswith('192.168.137.'):
|
||||
return 'worker', '🛰️'
|
||||
|
||||
# Port-based identification
|
||||
if 80 in open_ports or 443 in open_ports:
|
||||
if 3000 in open_ports: # Common for web apps
|
||||
return 'server', '💻'
|
||||
return 'server', '💻'
|
||||
|
||||
# Hostname patterns
|
||||
if 'router' in hostname_lower or 'gateway' in hostname_lower:
|
||||
return 'router', '🛜'
|
||||
elif 'iot' in hostname_lower or 'smart' in hostname_lower:
|
||||
return 'iot', '📺'
|
||||
elif 'laptop' in hostname_lower or 'pc' in hostname_lower:
|
||||
return 'laptop', '👨💻'
|
||||
elif 'nas' in hostname_lower or 'storage' in hostname_lower:
|
||||
return 'nas', '🧱'
|
||||
|
||||
return 'unknown', '❓'
|
||||
|
||||
def scan_network_range(self, network_cidr):
|
||||
"""Scan a specific IP range efficiently"""
|
||||
devices = []
|
||||
|
||||
try:
|
||||
network = ipaddress.ip_network(network_cidr, strict=False)
|
||||
|
||||
# For large networks, scan common ranges
|
||||
if network.num_addresses > 256:
|
||||
print(f" Large network ({network.num_addresses} addresses), scanning common ranges...")
|
||||
|
||||
# Scan first 50, last 50, and common server IPs
|
||||
hosts = list(network.hosts())
|
||||
scan_ips = []
|
||||
|
||||
# Common server IPs
|
||||
common_offsets = [1, 2, 10, 20, 50, 100, 150, 200, 254]
|
||||
for offset in common_offsets:
|
||||
if offset < len(hosts):
|
||||
scan_ips.append(str(hosts[offset]))
|
||||
|
||||
# First and last 20
|
||||
scan_ips.extend([str(ip) for ip in hosts[:20]])
|
||||
scan_ips.extend([str(ip) for ip in hosts[-20:]])
|
||||
|
||||
# Your specific IPs from the diagram
|
||||
specific_ips = [
|
||||
'192.168.1.1', '192.168.1.159', '192.168.1.163',
|
||||
'192.168.1.193', '192.168.1.100'
|
||||
]
|
||||
for ip in specific_ips:
|
||||
if ipaddress.ip_address(ip) in network:
|
||||
scan_ips.append(ip)
|
||||
|
||||
scan_ips = list(set(scan_ips)) # Remove duplicates
|
||||
|
||||
else:
|
||||
# For smaller networks, scan all
|
||||
scan_ips = [str(ip) for ip in network.hosts()]
|
||||
|
||||
print(f" Scanning {len(scan_ips)} IP addresses...")
|
||||
|
||||
# Scan in batches
|
||||
batch_size = 20
|
||||
for i in range(0, len(scan_ips), batch_size):
|
||||
batch = scan_ips[i:i + batch_size]
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_ip = {executor.submit(self.scan_single_device, ip): ip for ip in batch}
|
||||
|
||||
for future in concurrent.futures.as_completed(future_to_ip):
|
||||
device = future.result()
|
||||
if device:
|
||||
devices.append(device)
|
||||
print(f" Found: {device['icon']} {device['hostname']} ({device['ip']})")
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error scanning {network_cidr}: {e}")
|
||||
|
||||
return devices
|
||||
|
||||
def generate_mermaid_diagram(self, devices):
|
||||
"""Generate Mermaid diagram from discovered devices"""
|
||||
mermaid = "flowchart TD\n"
|
||||
|
||||
# Styles
|
||||
mermaid += "%% ---------- styles ----------\n"
|
||||
mermaid += "classDef internet fill:#e1f5ff,stroke:#007bff\n"
|
||||
mermaid += "classDef router fill:#fff3cd,stroke:#ffc107\n"
|
||||
mermaid += "classDef server fill:#ffe6e6,stroke:#dc3545\n"
|
||||
mermaid += "classDef dns fill:#e6ffe6,stroke:#28a745\n"
|
||||
mermaid += "classDef homeauto fill:#fff0f5,stroke:#e83e8c\n"
|
||||
mermaid += "classDef worker fill:#f0e6ff,stroke:#6f42c1\n"
|
||||
mermaid += "classDef iot fill:#fff9e6,stroke:#fd7e14\n"
|
||||
mermaid += "classDef unknown fill:#f8f9fa,stroke:#6c757d\n"
|
||||
mermaid += "\n"
|
||||
|
||||
# Group devices by network
|
||||
lan_devices = [d for d in devices if d['ip'].startswith('192.168.1.')]
|
||||
tether_devices = [d for d in devices if d['ip'].startswith('192.168.137.')]
|
||||
hyperv_devices = [d for d in devices if d['ip'].startswith('172.20.')]
|
||||
|
||||
# LAN network
|
||||
mermaid += f"subgraph LAN [🏠 LAN 192.168.1.0/24 - {len(lan_devices)} devices]\n"
|
||||
|
||||
# Core infrastructure first
|
||||
core_ips = ['192.168.1.1', '192.168.1.159', '192.168.1.163',
|
||||
'192.168.1.193', '192.168.1.100', '192.168.1.49', '192.168.1.165']
|
||||
|
||||
mermaid += " subgraph CORE [💻 Core Infrastructure]\n"
|
||||
for device in lan_devices:
|
||||
if device['ip'] in core_ips:
|
||||
services = ', '.join(device.get('services', []))[:30]
|
||||
mermaid += f" {device['ip'].replace('.', '_')}[\"{device['icon']} {device['description']}<br/>{device['ip']}<br/>{services}\"]:::server\n"
|
||||
mermaid += " end\n"
|
||||
|
||||
# Other LAN devices
|
||||
for device in lan_devices:
|
||||
if device['ip'] not in core_ips:
|
||||
node_id = device['ip'].replace('.', '_')
|
||||
services = ', '.join(device.get('services', []))[:20]
|
||||
mermaid += f" {node_id}[\"{device['icon']} {device['hostname']}<br/>{device['ip']}<br/>{device['vendor']}\"]:::unknown\n"
|
||||
|
||||
mermaid += "end\n\n"
|
||||
|
||||
# Tether network
|
||||
if tether_devices:
|
||||
mermaid += f"subgraph TETHER [📶 Tether 192.168.137.0/24 - {len(tether_devices)} devices]\n"
|
||||
|
||||
# Known workers first
|
||||
known_workers = ['192.168.137.239'] # Hermes
|
||||
for device in tether_devices:
|
||||
node_id = device['ip'].replace('.', '_')
|
||||
if device['ip'] in known_workers:
|
||||
mermaid += f" {node_id}[\"{device['icon']} {device['description']}<br/>{device['ip']}\"]:::worker\n"
|
||||
else:
|
||||
mermaid += f" {node_id}[\"{device['icon']} {device['hostname']}<br/>{device['ip']}\"]:::worker\n"
|
||||
|
||||
mermaid += "end\n\n"
|
||||
|
||||
# Hyper-V network
|
||||
if hyperv_devices:
|
||||
mermaid += f"subgraph HYPERV [🔷 Hyper-V 172.20.240.0/20 - {len(hyperv_devices)} devices]\n"
|
||||
for device in hyperv_devices:
|
||||
node_id = device['ip'].replace('.', '_')
|
||||
mermaid += f" {node_id}[\"{device['icon']} {device['hostname']}<br/>{device['ip']}\"]:::unknown\n"
|
||||
mermaid += "end\n\n"
|
||||
|
||||
# Connections
|
||||
mermaid += "%% ---------- connections ----------\n"
|
||||
mermaid += "CORE --> TETHER\n"
|
||||
mermaid += "CORE --> HYPERV\n"
|
||||
|
||||
return mermaid
|
||||
|
||||
def scan_single_device(self, ip):
|
||||
"""Scan a single device with enhanced identification"""
|
||||
try:
|
||||
if not self.windows_ping(ip):
|
||||
return None
|
||||
|
||||
# Get hostname
|
||||
try:
|
||||
hostname = socket.gethostbyaddr(ip)[0]
|
||||
except:
|
||||
hostname = ip
|
||||
|
||||
# Get MAC address
|
||||
mac = self.get_windows_mac(ip)
|
||||
|
||||
# Scan common ports
|
||||
common_ports = [21, 22, 23, 80, 443, 8080, 3000, 3389, 5000,
|
||||
8000, 8008, 8443, 9000, 9092]
|
||||
open_ports = self.scan_ports(ip, common_ports)
|
||||
|
||||
# Enhanced identification
|
||||
device_type, icon, description, services = self.better_identify_device(
|
||||
ip, hostname, open_ports, mac
|
||||
)
|
||||
|
||||
# Build device info
|
||||
device = {
|
||||
'ip': ip,
|
||||
'hostname': hostname,
|
||||
'mac': mac,
|
||||
'vendor': self.get_vendor_from_mac(mac) if mac else "Unknown",
|
||||
'open_ports': open_ports,
|
||||
'services': services,
|
||||
'type': device_type,
|
||||
'description': description,
|
||||
'icon': icon,
|
||||
'last_seen': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return device
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error scanning {ip}: {e}")
|
||||
return None
|
||||
|
||||
def discover_network(self):
|
||||
"""Main discovery function"""
|
||||
print("🔍 Starting Windows Network Discovery")
|
||||
print("=" * 50)
|
||||
|
||||
# Check admin rights
|
||||
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
if not is_admin:
|
||||
print("⚠️ Running without Administrator privileges.")
|
||||
print(" Some features (ARP scanning) may be limited.")
|
||||
print(" For best results, run as Administrator.\n")
|
||||
|
||||
# Get networks to scan
|
||||
networks = self.get_local_networks()
|
||||
print(f"📡 Networks to scan: {networks}\n")
|
||||
|
||||
all_devices = []
|
||||
|
||||
# Scan each network
|
||||
for network in networks:
|
||||
print(f"📶 Scanning {network}...")
|
||||
devices = self.scan_network_range(network)
|
||||
all_devices.extend(devices)
|
||||
print(f" Found {len(devices)} devices in {network}\n")
|
||||
|
||||
# Save results
|
||||
if all_devices:
|
||||
self.save_results(all_devices)
|
||||
else:
|
||||
print("❌ No devices found. Possible issues:")
|
||||
print(" 1. Firewall blocking ICMP (ping) requests")
|
||||
print(" 2. Devices not responding to ping")
|
||||
print(" 3. Network segmentation or VLANs")
|
||||
print("\n💡 Try:")
|
||||
print(" • Run as Administrator")
|
||||
print(" • Temporarily disable Windows Firewall")
|
||||
print(" • Check if devices are on same VLAN")
|
||||
|
||||
return all_devices
|
||||
|
||||
def enhance_service_detection(self, ip, open_ports):
|
||||
"""Enhanced service detection based on common port patterns"""
|
||||
services = []
|
||||
|
||||
port_services = {
|
||||
80: 'http', 443: 'https', 22: 'ssh', 23: 'telnet',
|
||||
21: 'ftp', 25: 'smtp', 53: 'dns', 67: 'dhcp',
|
||||
68: 'dhcp-client', 69: 'tftp', 88: 'kerberos',
|
||||
110: 'pop3', 119: 'nntp', 123: 'ntp', 135: 'msrpc',
|
||||
137: 'netbios-ns', 138: 'netbios-dgm', 139: 'netbios-ssn',
|
||||
143: 'imap', 161: 'snmp', 162: 'snmptrap', 389: 'ldap',
|
||||
445: 'smb', 465: 'smtps', 514: 'syslog', 587: 'smtp-submission',
|
||||
631: 'ipp', 636: 'ldaps', 993: 'imaps', 995: 'pop3s',
|
||||
1433: 'mssql', 1521: 'oracle', 1723: 'pptp', 1883: 'mqtt',
|
||||
1900: 'upnp', 2082: 'cpanel', 2083: 'cpanel-ssl',
|
||||
2086: 'whm', 2087: 'whm-ssl', 2095: 'webmail',
|
||||
2096: 'webmail-ssl', 2181: 'zookeeper', 2375: 'docker',
|
||||
2376: 'docker-ssl', 2377: 'docker-swarm', 2379: 'etcd',
|
||||
2380: 'etcd-peer', 3000: 'nodejs', 3306: 'mysql',
|
||||
3389: 'rdp', 5000: 'upnp/commplex-link', 5432: 'postgresql',
|
||||
5672: 'amqp', 5900: 'vnc', 5938: 'teamviewer',
|
||||
5984: 'couchdb', 6379: 'redis', 6443: 'kubernetes',
|
||||
6667: 'irc', 8000: 'http-alt', 8008: 'http-alt',
|
||||
8080: 'http-proxy', 8081: 'http-alt', 8443: 'https-alt',
|
||||
8888: 'http-alt', 9000: 'sonarqube', 9001: 'tor',
|
||||
9042: 'cassandra', 9092: 'kafka', 9100: 'pdl-datastream',
|
||||
9200: 'elasticsearch', 9300: 'elasticsearch-cluster',
|
||||
9418: 'git', 11211: 'memcached', 27017: 'mongodb',
|
||||
27018: 'mongodb-shard', 27019: 'mongodb-config',
|
||||
28017: 'mongodb-http', 50000: 'db2', 50070: 'hadoop-namenode',
|
||||
50075: 'hadoop-datanode', 50090: 'hadoop-secondarynamenode'
|
||||
}
|
||||
|
||||
for port in open_ports:
|
||||
if port in port_services:
|
||||
services.append(f"{port_services[port]}({port})")
|
||||
else:
|
||||
services.append(f"unknown({port})")
|
||||
|
||||
return services
|
||||
|
||||
def better_identify_device(self, ip, hostname, open_ports, mac):
|
||||
"""Improved device identification with service awareness"""
|
||||
hostname_lower = hostname.lower()
|
||||
services = self.enhance_service_detection(ip, open_ports)
|
||||
|
||||
# Your specific network devices
|
||||
if ip == '192.168.1.1':
|
||||
return 'router', '🛜', 'Router/Gateway', ['upnp(5000)']
|
||||
|
||||
elif ip == '192.168.1.159':
|
||||
core_services = []
|
||||
if 3000 in open_ports:
|
||||
core_services.append('gitea(3000)')
|
||||
if 443 in open_ports or 80 in open_ports:
|
||||
core_services.append('traefik(80/443)')
|
||||
if 22 in open_ports:
|
||||
core_services.append('ssh(22)')
|
||||
return 'server', '💻', 'Core Server (Traefik + Gitea)', core_services
|
||||
|
||||
elif ip == '192.168.1.163':
|
||||
infra_services = []
|
||||
if 8000 in open_ports:
|
||||
infra_services.append('artifactory(8000)')
|
||||
if 8080 in open_ports:
|
||||
infra_services.append('adguard(8080)')
|
||||
if 22 in open_ports:
|
||||
infra_services.append('ssh(22)')
|
||||
return 'dns', '🛡️', 'Infra (AdGuard + Artifactory)', infra_services
|
||||
|
||||
elif ip == '192.168.1.193':
|
||||
return 'home_automation', '🏡', 'Home Assistant', []
|
||||
|
||||
elif ip == '192.168.1.100':
|
||||
return 'worker', '🖥️', 'Atlas Worker Node', ['ssh(22)']
|
||||
|
||||
elif ip == '192.168.1.49':
|
||||
return 'server', '💻', 'Web Server', ['http(80)', 'https(443)', 'http-alt(8080)']
|
||||
|
||||
elif ip == '192.168.1.165':
|
||||
return 'server', '💻', 'HP Server/Printer', ['http(80)', 'https(443)', 'http-alt(8080)']
|
||||
|
||||
# Enhanced detection based on services
|
||||
if 8008 in open_ports and 8443 in open_ports:
|
||||
# Chromecast/Google Cast devices
|
||||
if mac and mac.startswith(('40:9f:38', 'f8:8f:ca', 'e4:f8:9c')):
|
||||
return 'iot', '📺', 'Chromecast/Google Device', ['cast(8008/8443)']
|
||||
return 'media', '📱', 'Media Device', ['cast(8008/8443)']
|
||||
|
||||
if 3389 in open_ports:
|
||||
return 'laptop', '💻', 'Windows PC (RDP)', ['rdp(3389)']
|
||||
|
||||
if 22 in open_ports and ip.startswith('192.168.137.'):
|
||||
# Tether network workers
|
||||
if ip == '192.168.137.239' and hostname == 'hermes.lan':
|
||||
return 'worker', '🛰️', 'Hermes Worker', ['ssh(22)']
|
||||
return 'worker', '🛰️', 'Tether Worker Node', ['ssh(22)']
|
||||
|
||||
# MAC address vendor lookup
|
||||
if mac:
|
||||
vendor = self.get_vendor_from_mac(mac)
|
||||
if any(v in vendor.lower() for v in ['sonos', 'roku', 'philips hue', 'smart']):
|
||||
return 'iot', '🏠', f'IoT ({vendor})', services
|
||||
elif 'raspberry' in vendor.lower():
|
||||
return 'server', '🍓', f'Raspberry Pi', services
|
||||
elif any(v in vendor.lower() for v in ['cisco', 'ubiquiti', 'tplink', 'netgear']):
|
||||
return 'network', '📡', f'Network Device ({vendor})', services
|
||||
|
||||
# Default based on IP range
|
||||
if ip.startswith('192.168.137.'):
|
||||
return 'worker', '🛰️', 'Tether Network Device', services
|
||||
elif ip.startswith('172.20.'):
|
||||
return 'hyperv', '🔷', 'Hyper-V Internal Network', services
|
||||
|
||||
return 'unknown', '❓', 'Unknown Device', services
|
||||
|
||||
def get_vendor_from_mac(self, mac):
|
||||
"""Get vendor from MAC address OUI"""
|
||||
if not mac:
|
||||
return "Unknown"
|
||||
|
||||
# Clean MAC address
|
||||
mac_clean = mac.replace(':', '').replace('-', '').upper()[:6]
|
||||
|
||||
# Common OUI prefixes
|
||||
oui_db = {
|
||||
'B8D526': 'ASUSTek', '409F38': 'Google', 'ECB5FA': 'Giga-Byte Technology',
|
||||
'4827E2': 'Huawei', '0417B6': 'ASUSTek', 'EAE7A7': 'Shenzhen Bolutek',
|
||||
'CCF411': 'Murata Manufacturing', 'B04A39': 'Xiaomi', '50EBF6': 'ASUSTek',
|
||||
'34CDB0': 'Huawei', '001E06': 'Actiontec', '48BA4E': 'Hewlett Packard',
|
||||
'E4B97A': 'Huawei', '7CDFA1': 'Huawei', '107B44': 'TP-Link',
|
||||
'4C4929': 'Huawei', 'AC6784': 'Huawei', '1831BF': 'Apple',
|
||||
'382CE5': 'Samsung', 'FEE772': 'Samsung', '4CBAD7': 'Huawei',
|
||||
'C8C9A3': 'LG Electronics', 'C45BBE': 'PEGATRON'
|
||||
}
|
||||
|
||||
return oui_db.get(mac_clean, "Unknown Manufacturer")
|
||||
|
||||
def save_results(self, devices):
|
||||
"""Save results to JSON and generate summary"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# JSON output
|
||||
output = {
|
||||
'discovery_time': datetime.now().isoformat(),
|
||||
'total_devices': len(devices),
|
||||
'networks_scanned': self.get_local_networks(),
|
||||
'devices': devices
|
||||
}
|
||||
|
||||
json_file = f"network_discovery_{timestamp}.json"
|
||||
with open(json_file, 'w') as f:
|
||||
json.dump(output, f, indent=2)
|
||||
|
||||
# Generate summary report
|
||||
summary_file = f"network_summary_{timestamp}.txt"
|
||||
with open(summary_file, 'w') as f:
|
||||
f.write("=" * 60 + "\n")
|
||||
f.write("NETWORK DISCOVERY SUMMARY\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
|
||||
f.write(f"Discovery Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"Total Devices Found: {len(devices)}\n\n")
|
||||
|
||||
# Group by type
|
||||
type_count = {}
|
||||
for device in devices:
|
||||
dev_type = device['type']
|
||||
type_count[dev_type] = type_count.get(dev_type, 0) + 1
|
||||
|
||||
f.write("Devices by Type:\n")
|
||||
f.write("-" * 40 + "\n")
|
||||
for dev_type, count in sorted(type_count.items()):
|
||||
icon = self.icon_mapping.get(dev_type, '❓')
|
||||
f.write(f"{icon} {dev_type}: {count}\n")
|
||||
|
||||
f.write("\n" + "=" * 60 + "\n")
|
||||
f.write("DETAILED DEVICE LIST\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
|
||||
for device in devices:
|
||||
f.write(f"{device['icon']} {device['hostname']} ({device['ip']})\n")
|
||||
f.write(f" Type: {device['type']}\n")
|
||||
f.write(f" MAC: {device.get('mac', 'Unknown')}\n")
|
||||
if device['open_ports']:
|
||||
f.write(f" Open Ports: {', '.join(map(str, device['open_ports']))}\n")
|
||||
f.write("\n")
|
||||
|
||||
print("=" * 50)
|
||||
print("✅ DISCOVERY COMPLETE")
|
||||
print("=" * 50)
|
||||
print(f"📊 Found {len(devices)} total devices")
|
||||
print(f"💾 Results saved to:")
|
||||
print(f" • {json_file} (JSON data)")
|
||||
print(f" • {summary_file} (Text summary)")
|
||||
|
||||
# Print quick summary
|
||||
print("\n📋 Quick Summary:")
|
||||
type_count = {}
|
||||
for device in devices:
|
||||
dev_type = device['type']
|
||||
type_count[dev_type] = type_count.get(dev_type, 0) + 1
|
||||
|
||||
for dev_type, count in sorted(type_count.items()):
|
||||
icon = self.icon_mapping.get(dev_type, '❓')
|
||||
print(f" {icon} {dev_type}: {count}")
|
||||
|
||||
|
||||
# Run the discovery
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
discoverer = WindowsNetworkDiscoverer()
|
||||
discoverer.discover_network()
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ Discovery interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
14
src/ico.py
Normal file
14
src/ico.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Convert SVG to PNG first
|
||||
png_path = "../public/favicon.png"
|
||||
ico_path = "../public/favicon.ico"
|
||||
|
||||
cairosvg.svg2png(url="../public/favicon.svg", write_to=png_path)
|
||||
|
||||
# Convert PNG → ICO
|
||||
img = Image.open(png_path)
|
||||
img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
|
||||
|
||||
ico_path
|
||||
175
src/json_to_mermaid.py
Normal file
175
src/json_to_mermaid.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def json_to_mermaid(json_file_path, output_mmd_path='network_diagram.mmd'):
|
||||
"""
|
||||
Converts the network discovery JSON to a Mermaid flowchart.
|
||||
"""
|
||||
with open(json_file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
mermaid_lines = []
|
||||
mermaid_lines.append('flowchart TD')
|
||||
|
||||
# 1. Define styling classes
|
||||
mermaid_lines.append(' %% ---------- styles ----------')
|
||||
mermaid_lines.append(' classDef internet fill:#e1f5ff,stroke:#007bff')
|
||||
mermaid_lines.append(' classDef router fill:#fff3cd,stroke:#ffc107')
|
||||
mermaid_lines.append(' classDef server fill:#ffe6e6,stroke:#dc3545')
|
||||
mermaid_lines.append(' classDef dns fill:#e6ffe6,stroke:#28a745')
|
||||
mermaid_lines.append(' classDef homeauto fill:#fff0f5,stroke:#e83e8c')
|
||||
mermaid_lines.append(' classDef worker fill:#f0e6ff,stroke:#6f42c1')
|
||||
mermaid_lines.append(' classDef iot fill:#fff9e6,stroke:#fd7e14')
|
||||
mermaid_lines.append(' classDef unknown fill:#f8f9fa,stroke:#6c757d')
|
||||
mermaid_lines.append('')
|
||||
|
||||
# 2. Create a mapping for device type to CSS class
|
||||
type_to_class = {
|
||||
'router': 'router',
|
||||
'server': 'server',
|
||||
'dns': 'dns',
|
||||
'home_automation': 'homeauto',
|
||||
'worker': 'worker',
|
||||
'iot': 'iot'
|
||||
}
|
||||
|
||||
# 3. Group devices by network
|
||||
lan_devices = []
|
||||
tether_devices = []
|
||||
hyperv_devices = []
|
||||
other_devices = []
|
||||
|
||||
for device in data['devices']:
|
||||
ip = device.get('ip', '')
|
||||
if ip.startswith('192.168.1.'):
|
||||
lan_devices.append(device)
|
||||
elif ip.startswith('192.168.137.'):
|
||||
tether_devices.append(device)
|
||||
elif ip.startswith('172.20.'):
|
||||
hyperv_devices.append(device)
|
||||
else:
|
||||
other_devices.append(device)
|
||||
|
||||
# 4. Create LAN Subgraph
|
||||
mermaid_lines.append(f' subgraph LAN["LAN 192.168.1.0/24 - {len(lan_devices)} devices"]')
|
||||
|
||||
# Core infrastructure first (from your original diagram)
|
||||
core_ips = ['192.168.1.1', '192.168.1.159', '192.168.1.163',
|
||||
'192.168.1.193', '192.168.1.100', '192.168.1.49', '192.168.1.165']
|
||||
|
||||
# Add a core server subgraph
|
||||
mermaid_lines.append(' subgraph CORE["Core Infrastructure"]')
|
||||
for device in lan_devices:
|
||||
if device['ip'] in core_ips:
|
||||
node_id = device['ip'].replace('.', '_')
|
||||
# Use ASCII-friendly labels instead of emoji for Windows compatibility
|
||||
icon_map = {
|
||||
'router': '[Router]',
|
||||
'server': '[Server]',
|
||||
'dns': '[DNS]',
|
||||
'home_automation': '[Home]',
|
||||
'worker': '[Worker]',
|
||||
'unknown': '[?]'
|
||||
}
|
||||
icon = icon_map.get(device.get('type', 'unknown'), '[?]')
|
||||
label = f"{icon} {device.get('hostname', device['ip'])}"
|
||||
mermaid_lines.append(f' {node_id}["{label}<br/>{device["ip"]}"]')
|
||||
|
||||
# Apply CSS class
|
||||
dev_type = device.get('type', 'unknown')
|
||||
css_class = type_to_class.get(dev_type, 'unknown')
|
||||
mermaid_lines.append(f' class {node_id} {css_class};')
|
||||
mermaid_lines.append(' end')
|
||||
|
||||
# Other LAN devices
|
||||
for device in lan_devices:
|
||||
if device['ip'] not in core_ips:
|
||||
node_id = device['ip'].replace('.', '_')
|
||||
icon_map = {
|
||||
'router': '[R]',
|
||||
'server': '[S]',
|
||||
'dns': '[D]',
|
||||
'home_automation': '[H]',
|
||||
'worker': '[W]',
|
||||
'unknown': '[?]'
|
||||
}
|
||||
icon = icon_map.get(device.get('type', 'unknown'), '[?]')
|
||||
label = f"{icon} {device.get('hostname', device['ip'])}"
|
||||
mermaid_lines.append(f' {node_id}["{label}<br/>{device["ip"]}"]')
|
||||
|
||||
dev_type = device.get('type', 'unknown')
|
||||
css_class = type_to_class.get(dev_type, 'unknown')
|
||||
mermaid_lines.append(f' class {node_id} {css_class};')
|
||||
|
||||
mermaid_lines.append(' end')
|
||||
mermaid_lines.append('')
|
||||
|
||||
# 5. Create TETHER Subgraph
|
||||
if tether_devices:
|
||||
mermaid_lines.append(f' subgraph TETHER["Tether 192.168.137.0/24 - {len(tether_devices)} devices"]')
|
||||
|
||||
# Known workers first
|
||||
known_workers = ['192.168.137.239'] # Hermes
|
||||
for device in tether_devices:
|
||||
node_id = device['ip'].replace('.', '_')
|
||||
if device['ip'] in known_workers:
|
||||
label = f"[Hermes] {device.get('hostname', device['ip'])}<br/>{device['ip']}"
|
||||
else:
|
||||
label = f"[Worker] {device.get('hostname', device['ip'])}<br/>{device['ip']}"
|
||||
mermaid_lines.append(f' {node_id}["{label}"]')
|
||||
mermaid_lines.append(f' class {node_id} worker;')
|
||||
|
||||
mermaid_lines.append(' end')
|
||||
mermaid_lines.append('')
|
||||
|
||||
# 6. Add connections
|
||||
mermaid_lines.append(' %% ---------- Connections ----------')
|
||||
# Connect core server to tether network
|
||||
if tether_devices and any(d['ip'] == '192.168.1.159' for d in lan_devices):
|
||||
mermaid_lines.append(' 192_168_1_159 -.-> TETHER')
|
||||
# Router connects to everything
|
||||
if any(d['ip'] == '192.168.1.1' for d in lan_devices):
|
||||
mermaid_lines.append(' 192_168_1_1 --> LAN')
|
||||
|
||||
# 7. Write the Mermaid code to a file with UTF-8 encoding
|
||||
with open(output_mmd_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(mermaid_lines))
|
||||
|
||||
print(f"✓ Mermaid diagram generated: {output_mmd_path}")
|
||||
print(f" Devices: {len(lan_devices)} LAN, {len(tether_devices)} Tether")
|
||||
if hyperv_devices:
|
||||
print(f" {len(hyperv_devices)} Hyper-V")
|
||||
print(f" Open {output_mmd_path} and copy contents to https://mermaid.live")
|
||||
return output_mmd_path
|
||||
|
||||
|
||||
# --- Run the conversion with command-line support ---
|
||||
if __name__ == "__main__":
|
||||
# Default filenames
|
||||
input_json = "network_discovery_20251207_160645.json"
|
||||
output_mmd = "network_topology.mmd"
|
||||
|
||||
# Check for command-line arguments
|
||||
if len(sys.argv) > 1:
|
||||
input_json = sys.argv[1]
|
||||
if len(sys.argv) > 2:
|
||||
output_mmd = sys.argv[2]
|
||||
|
||||
try:
|
||||
mmd_file = json_to_mermaid(input_json, output_mmd)
|
||||
except FileNotFoundError:
|
||||
print(f"✗ Error: Could not find file '{input_json}'")
|
||||
print(" Available JSON files:")
|
||||
import os
|
||||
|
||||
for file in os.listdir('.'):
|
||||
if file.endswith('.json'):
|
||||
print(f" - {file}")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"✗ Error: Invalid JSON in '{input_json}': {e}")
|
||||
except Exception as e:
|
||||
print(f"✗ Unexpected error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
121
src/lan_architecture.py
Normal file
121
src/lan_architecture.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from diagrams import Diagram, Cluster, Edge
|
||||
from diagrams.onprem.network import Internet
|
||||
from diagrams.onprem.compute import Server
|
||||
from diagrams.onprem.iac import Ansible
|
||||
from diagrams.generic.network import Router, Switch
|
||||
from diagrams.generic.device import Mobile, Tablet
|
||||
from diagrams.generic.blank import Blank
|
||||
from diagrams.onprem.client import Users
|
||||
from diagrams.onprem.container import Docker
|
||||
from diagrams.custom import Custom
|
||||
|
||||
# Tip: run this in a venv:
|
||||
# pip install diagrams graphviz
|
||||
|
||||
with Diagram("Home Lab / Auction Stack Architecture", show=False, filename="home_lab_architecture", direction="LR"):
|
||||
internet = Internet("Internet / Cloud")
|
||||
|
||||
dev = Users("Dev laptop(s)\nWindows / WSL")
|
||||
|
||||
# -------------------- LAN 192.168.1.x --------------------
|
||||
with Cluster("LAN 192.168.1.0/24"):
|
||||
router = Router("hub.lan\n192.168.1.1\nRouter / Gateway")
|
||||
|
||||
# -------- Core server / desktop (Tour / hephaestus / dokku / ollama) --------
|
||||
with Cluster("Core server / desktop\nTour / hephaestus / dokku.lan / ollama.lan\n192.168.1.159"):
|
||||
core_os = Server("Ubuntu host")
|
||||
|
||||
traefik = Docker("Traefik\nReverse Proxy")
|
||||
gitea = Docker("Gitea\n git.appmodel.nl")
|
||||
dokku = Docker("Dokku\nPaaS / app hosting")
|
||||
auction_fe = Docker("Auction Frontend\n auction.appmodel.nl")
|
||||
aupi_be = Docker("Aupi API Backend\n aupi.appmodel.nl")
|
||||
mi50 = Server("MI50 GPU / Ollama\nAI workloads")
|
||||
|
||||
# -------- Infra & DNS (odroid / dns.lan) --------
|
||||
with Cluster("Infra & DNS\nodroid / dns.lan\n192.168.1.163"):
|
||||
dns_host = Server("Odroid host")
|
||||
adguard = Docker("AdGuard Home\nDNS / *.lan / *.appmodel.nl")
|
||||
artifactory = Docker("Artifactory (future)")
|
||||
runner = Docker("CI / Build runner (future)")
|
||||
|
||||
# -------- Home Assistant --------
|
||||
with Cluster("Home Automation\nha.lan\n192.168.1.193"):
|
||||
ha_host = Server("HomeAssistant host")
|
||||
hass = Docker("Home Assistant")
|
||||
|
||||
# -------- Extra node / worker --------
|
||||
atlas = Server("atlas.lan\n192.168.1.100\n(extra node / worker)")
|
||||
|
||||
# -------- IoT / devices --------
|
||||
with Cluster("IoT / Clients"):
|
||||
iot_hof = Tablet("hof-E402NA\n192.168.1.214")
|
||||
iot_s380 = Tablet("S380HB\n192.168.1.59")
|
||||
iot_ecb5 = Tablet("ecb5faa56c90\n192.168.1.49")
|
||||
iot_unknown = Tablet("Unknown\n192.168.1.240")
|
||||
|
||||
# -------------------- Tether subnet 192.168.137.x --------------------
|
||||
with Cluster("Tether subnet 192.168.137.0/24"):
|
||||
hermes = Server("hermes.lan\n192.168.137.239\nworker / node")
|
||||
plato = Server("plato.lan\n192.168.137.163\nworker / node")
|
||||
|
||||
# -------------------- Edges / flows --------------------
|
||||
|
||||
# Internet naar router + DNS host
|
||||
internet >> router
|
||||
internet >> dns_host
|
||||
|
||||
# Dev naar Gitea / Traefik / Dokku
|
||||
dev >> Edge(label="git / HTTPS") >> traefik
|
||||
dev >> Edge(label="SSH / HTTPS") >> gitea
|
||||
dev >> Edge(label="Dokku deploys") >> dokku
|
||||
|
||||
# Router naar alle LAN-nodes
|
||||
router >> core_os
|
||||
router >> dns_host
|
||||
router >> ha_host
|
||||
router >> atlas
|
||||
router >> iot_hof
|
||||
router >> iot_s380
|
||||
router >> iot_ecb5
|
||||
router >> iot_unknown
|
||||
|
||||
# Core server services
|
||||
core_os >> traefik
|
||||
core_os >> gitea
|
||||
core_os >> dokku
|
||||
core_os >> auction_fe
|
||||
core_os >> aupi_be
|
||||
core_os >> mi50
|
||||
|
||||
# Infra/DNS services
|
||||
dns_host >> adguard
|
||||
dns_host >> artifactory
|
||||
dns_host >> runner
|
||||
|
||||
# Home Assistant
|
||||
ha_host >> hass
|
||||
|
||||
# DNS-queries
|
||||
core_os >> Edge(label="DNS") >> adguard
|
||||
ha_host >> Edge(label="DNS") >> adguard
|
||||
atlas >> Edge(label="DNS") >> adguard
|
||||
hermes >> Edge(label="DNS") >> adguard
|
||||
plato >> Edge(label="DNS") >> adguard
|
||||
|
||||
# Web traffic / reverse proxy flows
|
||||
internet >> Edge(label="HTTP/HTTPS") >> traefik
|
||||
traefik >> Edge(label="git.appmodel.nl") >> gitea
|
||||
traefik >> Edge(label="auction.appmodel.nl") >> auction_fe
|
||||
traefik >> Edge(label="aupi.appmodel.nl") >> aupi_be
|
||||
traefik >> Edge(label="dokku.lan / apps") >> dokku
|
||||
|
||||
# App-level flow
|
||||
auction_fe >> Edge(label="REST API") >> aupi_be
|
||||
|
||||
# AI workloads
|
||||
dev >> Edge(label="LLM / Tuning / Inference") >> mi50
|
||||
|
||||
# Tether workers verbonden met core (bv. jobs / agents)
|
||||
core_os >> Edge(label="jobs / ssh") >> hermes
|
||||
core_os >> Edge(label="jobs / ssh") >> plato
|
||||
159
src/main.py
Normal file
159
src/main.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from graphviz import Digraph
|
||||
|
||||
def make_network_diagram(output_name: str = "network-architecture"):
|
||||
g = Digraph(
|
||||
"network",
|
||||
filename=f"{output_name}.gv",
|
||||
format="png",
|
||||
)
|
||||
|
||||
# Globale stijl
|
||||
g.attr(rankdir="LR", fontname="Segoe UI")
|
||||
|
||||
# ---------- WAN / Internet ----------
|
||||
with g.subgraph(name="cluster_wan") as wan:
|
||||
wan.attr(
|
||||
label="🌐 Internet / Cloud",
|
||||
style="rounded",
|
||||
color="lightgrey",
|
||||
fontsize="16",
|
||||
)
|
||||
wan.node("extDNS", "📡 Public DNS", shape="rectangle")
|
||||
wan.node("extGit", "☁️ Externe registries / Git\n(GitHub / Docker Hub)", shape="rectangle")
|
||||
|
||||
# ---------- LAN 192.168.1.0/24 ----------
|
||||
with g.subgraph(name="cluster_lan") as lan:
|
||||
lan.attr(
|
||||
label="🏠 LAN 192.168.1.0/24",
|
||||
style="rounded",
|
||||
color="lightgrey",
|
||||
fontsize="16",
|
||||
)
|
||||
|
||||
# Router / gateway
|
||||
lan.node(
|
||||
"hub",
|
||||
"🛜 Router / Gateway\nhub.lan\n192.168.1.1",
|
||||
shape="rectangle",
|
||||
style="filled",
|
||||
fillcolor="#f0f0ff",
|
||||
)
|
||||
|
||||
# ---- Core server / desktop ----
|
||||
with lan.subgraph(name="cluster_core") as core:
|
||||
core.attr(
|
||||
label="💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159",
|
||||
style="rounded",
|
||||
color="#aaccee",
|
||||
)
|
||||
core.node("traefik", "🚦 Traefik\nReverse Proxy", shape="rectangle")
|
||||
core.node("gitea", "📚 Gitea\ngit.appmodel.nl", shape="rectangle")
|
||||
core.node("dokku", "🐳 Dokku\nPaaS / build", shape="rectangle")
|
||||
core.node("auctionFE", "🧱 Auction Frontend\nauction.appmodel.nl", shape="rectangle")
|
||||
core.node("aupiAPI", "🧱 Auction Backend API\naupi.appmodel.nl", shape="rectangle")
|
||||
core.node("mi50", "🧠 MI50 / Ollama\nAI workloads", shape="rectangle")
|
||||
|
||||
# Aanvulling: monitoring / logging
|
||||
core.node("monitoring", "📈 Monitoring / Logging\nPrometheus / Loki / Grafana", shape="rectangle")
|
||||
|
||||
# ---- Infra & DNS ----
|
||||
with lan.subgraph(name="cluster_infra_dns") as infra:
|
||||
infra.attr(
|
||||
label="🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163",
|
||||
style="rounded",
|
||||
color="#aaddaa",
|
||||
)
|
||||
infra.node("adguard", "🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl", shape="rectangle")
|
||||
infra.node("artifactory", "📦 Artifactory", shape="rectangle")
|
||||
infra.node("runner", "⚙️ Build runners\nCI/CD", shape="rectangle")
|
||||
|
||||
# ---- Home Automation ----
|
||||
with lan.subgraph(name="cluster_ha") as ha:
|
||||
ha.attr(
|
||||
label="🏡 Home Automation\nha.lan\n192.168.1.193",
|
||||
style="rounded",
|
||||
color="#ffddaa",
|
||||
)
|
||||
ha.node("hass", "🏠 Home Assistant", shape="rectangle")
|
||||
|
||||
# Overige LAN-hosts / IoT
|
||||
lan.node("atlas", "🧱 atlas.lan\n192.168.1.100", shape="rectangle")
|
||||
lan.node("iot1", "📺 hof-E402NA\n192.168.1.214", shape="rectangle")
|
||||
lan.node("iot2", "🎧 S380HB\n192.168.1.59", shape="rectangle")
|
||||
lan.node("iot3", "📟 ecb5faa56c90\n192.168.1.49", shape="rectangle")
|
||||
lan.node("iot4", "❓ Unknown\n192.168.1.240", shape="rectangle")
|
||||
|
||||
# ---------- Tether subnet ----------
|
||||
with g.subgraph(name="cluster_tether") as tether:
|
||||
tether.attr(
|
||||
label="📶 Tether subnet 192.168.137.0/24",
|
||||
style="rounded",
|
||||
color="lightgrey",
|
||||
fontsize="16",
|
||||
)
|
||||
tether.node("hermes", "🛰️ hermes.lan\n192.168.137.239\nworker / node", shape="rectangle")
|
||||
tether.node("plato", "🛰️ plato.lan\n192.168.137.163\nworker / node", shape="rectangle")
|
||||
|
||||
# ---------- Externe gebruikers (aanvulling) ----------
|
||||
g.node(
|
||||
"users",
|
||||
"👨💻 Developers / Users\nlaptops / mobiel",
|
||||
shape="rectangle",
|
||||
style="dashed",
|
||||
)
|
||||
|
||||
# ==================== VERBINDINGEN ====================
|
||||
|
||||
# Basis LAN connecties (ongeveer jouw '---' links)
|
||||
for target in ["core", "infraDNS", "ha", "atlas", "iot1", "iot2", "iot3", "iot4"]:
|
||||
# We linken naar representatieve node binnen subgraph
|
||||
if target == "core":
|
||||
g.edge("hub", "traefik", dir="both", label="LAN")
|
||||
elif target == "infraDNS":
|
||||
g.edge("hub", "adguard", dir="both", label="LAN")
|
||||
elif target == "ha":
|
||||
g.edge("hub", "hass", dir="both", label="LAN")
|
||||
else:
|
||||
g.edge("hub", target, dir="both", label="LAN")
|
||||
|
||||
# WAN koppeling
|
||||
g.edge("hub", "extDNS", label="📶 Internet")
|
||||
g.edge("adguard", "extDNS", label="Upstream DNS")
|
||||
|
||||
# DNS-resolutie (core / ha / atlas / tether -> AdGuard)
|
||||
for client in ["traefik", "hass", "atlas", "hermes", "plato"]:
|
||||
g.edge(client, "adguard", label="DNS", style="dashed")
|
||||
|
||||
# Websites / reverse proxy
|
||||
g.edge("extDNS", "traefik", label="DNS → Traefik")
|
||||
g.edge("traefik", "gitea", label="HTTP(S)")
|
||||
g.edge("traefik", "auctionFE", label="HTTP(S)")
|
||||
g.edge("traefik", "aupiAPI", label="HTTP(S)")
|
||||
g.edge("traefik", "dokku", label="Apps / Deploy")
|
||||
|
||||
# App flow
|
||||
g.edge("auctionFE", "aupiAPI", label="API calls")
|
||||
g.edge("aupiAPI", "adguard", label="DNS lookups", style="dashed")
|
||||
|
||||
# AI workloads
|
||||
g.edge("traefik", "mi50", label="AI / inference", style="dotted")
|
||||
|
||||
# Tether workers
|
||||
g.edge("traefik", "hermes", dir="both", label="Jobs / tasks")
|
||||
g.edge("traefik", "plato", dir="both", label="Jobs / tasks")
|
||||
|
||||
# Monitoring / logging (aanvulling)
|
||||
for observed in ["traefik", "gitea", "dokku", "auctionFE", "aupiAPI", "mi50", "adguard", "atlas", "hass"]:
|
||||
g.edge(observed, "monitoring", style="dotted", label="metrics / logs")
|
||||
|
||||
# Developers / gebruikers
|
||||
g.edge("users", "traefik", label="HTTPS")
|
||||
g.edge("users", "gitea", style="dashed", label="Git / HTTP")
|
||||
|
||||
# Genereer bestanden (PNG + .gv)
|
||||
g.render(cleanup=True)
|
||||
print(f"Diagram gegenereerd als {output_name}.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
make_network_diagram()
|
||||
Reference in New Issue
Block a user