This commit is contained in:
Tour
2025-12-08 09:08:43 +01:00
commit 067abd1280
30 changed files with 5348 additions and 0 deletions

42
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
public/img/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

266
public/index.html Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
diagrams==0.25.1
graphviz==0.20.3

View 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"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
============================================================
NETWORK DISCOVERY SUMMARY
============================================================
Discovery Time: 2025-12-07 16:06:45
Total Devices Found: 76
Devices by Type:
----------------------------------------

View File

@@ -0,0 +1,9 @@
============================================================
NETWORK DISCOVERY SUMMARY
============================================================
Discovery Time: 2025-12-07 16:35:12
Total Devices Found: 96
Devices by Type:
----------------------------------------

View 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
View 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
View 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
View 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
View 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
View 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()