feat(stack): replace Organizr with Tatlock UI, remove Netdata
Remove Organizr dashboard and Netdata monitoring: - Delete stacks/organizr.yml and stacks/netdata.yml - Delete organizr-widgets/ directory and npm forward-auth config - Remove organizr database references from postgres-shared docs Promote Tatlock UI as primary dashboard: - Move from port 8092 to 9999 (Organizr's port) - Enable external access at home.schweitz.net - Update all documentation references Update service counts: 26 containers across 20 stacks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,10 +39,10 @@ This is the `tower-of-joy` project - a containerized home server infrastructure
|
||||
- **Stack Management** the portainer (and docker) and NPM services are managed through the core-api service. To maintain settings and configurations of these systems, read the documentation at http://tower-of-joy:8083/docs and prefer to use the api functions over direct reads an edits.
|
||||
|
||||
**Service Integration Policy:** A service deployment is INCOMPLETE until cross-service integrations are implemented. Every new service MUST be integrated with:
|
||||
- **Organizr:** Configure service in dashboard (Settings → Tab Editor, Homepage Items)
|
||||
- **Tatlock UI:** Add service to dashboard configuration
|
||||
- **docs/reference/CONTAINERS.md:** Document the service with full profile and configuration table
|
||||
|
||||
Services without monitoring and dashboard integration are considered unfinished and should not be marked as "complete" in STATUS.md or commit messages.
|
||||
Services without dashboard integration are considered unfinished and should not be marked as "complete" in STATUS.md or commit messages.
|
||||
|
||||
## Build & Run Commands
|
||||
|
||||
@@ -355,11 +355,9 @@ When deploying a NEW service, follow this complete checklist. A deployment is **
|
||||
- [ ] Document access credentials securely
|
||||
|
||||
**Phase 3: Cross-Service Integration (MANDATORY)**
|
||||
- [ ] **Organizr Integration:**
|
||||
- Add service URL and API token to Organizr (Settings → Tab Editor)
|
||||
- Enable homepage widgets if supported
|
||||
- Create service tab for direct access
|
||||
- Test widget displays data correctly
|
||||
- [ ] **Tatlock UI Integration:**
|
||||
- Add service to dashboard configuration
|
||||
- Test service appears correctly in dashboard
|
||||
- [ ] **NPM Integration (if externally accessible):**
|
||||
- Create proxy host entry
|
||||
- Configure SSL with Let's Encrypt
|
||||
@@ -380,7 +378,7 @@ When deploying a NEW service, follow this complete checklist. A deployment is **
|
||||
**Phase 5: Verification**
|
||||
- [ ] Service accessible at documented URL
|
||||
- [ ] Docker healthcheck shows healthy status
|
||||
- [ ] Organizr displays service widget/tab correctly
|
||||
- [ ] Tatlock UI displays service correctly
|
||||
- [ ] Service persists across container restart
|
||||
- [ ] Backups configured (if service has important data)
|
||||
|
||||
@@ -388,7 +386,7 @@ When deploying a NEW service, follow this complete checklist. A deployment is **
|
||||
```bash
|
||||
# 1. Deploy Jellyfin container
|
||||
# 2. Configure Jellyfin settings and add media
|
||||
# 3. Add Jellyfin to Organizr (homepage widgets + tab)
|
||||
# 3. Add Jellyfin to Tatlock UI dashboard
|
||||
# 4. Document in CONTAINERS.md
|
||||
# 5. Test all integrations work
|
||||
# ✓ NOW the deployment is complete
|
||||
|
||||
+14
-89
@@ -1,7 +1,7 @@
|
||||
# Container Reference - tower-of-joy Infrastructure
|
||||
|
||||
> **Last Updated:** 2026-01-02
|
||||
> **Total Services:** 28 containers across 22 stacks
|
||||
> **Last Updated:** 2026-01-03
|
||||
> **Total Services:** 26 containers across 20 stacks
|
||||
> **System:** Intel i7-6700, RTX 2080 Ti (11GB VRAM), 64GB RAM, Zorin OS 16.3
|
||||
|
||||
---
|
||||
@@ -17,8 +17,6 @@
|
||||
| **Redis Shared** | 6379 | N/A (internal) | No | No | - | ✅ Running |
|
||||
| **Ollama** | 11434 | http://192.168.86.149:11434 | LAN | Yes (RTX 2080 Ti) | - | ✅ Running |
|
||||
| **Headscale** | 8085, 9090 | http://192.168.86.149:8085 | LAN | No | - | ✅ Running |
|
||||
| **Netdata** | 19999 | http://192.168.86.149:19999 | LAN | No | - | ✅ Running |
|
||||
| **Organizr** | 9999 | https://home.schweitz.net | Internet (SSO) | No | - | ✅ Running |
|
||||
| **Watchtower** | None | N/A (background) | No | No | - | ✅ Running |
|
||||
| **Open WebUI** | 82 | https://webui.schweitz.net | Internet (SSO) | No | - | ✅ Running |
|
||||
| **Core API** | 8083 | http://192.168.86.149:8083 | LAN | No | - | ✅ Running (external) |
|
||||
@@ -37,10 +35,10 @@
|
||||
| **Paperless-ngx** | 8091 | https://documents.schweitz.net | Internet | No | 8 | ✅ Running |
|
||||
| **ClamAV** | 3310 | N/A (host service) | No | No | - | ✅ Running |
|
||||
| **AdGuard Home** | 53, 3053 | http://dns.schweitz.internal | LAN (DNS) | No | - | ✅ Running |
|
||||
| **Tatlock UI** | 8092 | http://192.168.86.149:8092 | LAN (future: home.schweitz.net) | No | - | ✅ Running |
|
||||
| **Tatlock UI** | 9999 | https://home.schweitz.net | Internet | No | - | ✅ Running |
|
||||
|
||||
### External Domains (SSL via Let's Encrypt)
|
||||
- **home.schweitz.net** → Organizr (Protected by Authentik SSO)
|
||||
- **home.schweitz.net** → Tatlock UI
|
||||
- **media.schweitz.net** → Jellyfin
|
||||
- **cloud.schweitz.net** → Nextcloud
|
||||
- **git.schweitz.net** → Gitea
|
||||
@@ -154,22 +152,13 @@ PostgreSQL Shared is a centralized PostgreSQL 17 database server providing isola
|
||||
| **Resource Limits** | None |
|
||||
| **GPU Required** | No |
|
||||
| **Dependencies** | docker-dataplane network |
|
||||
| **Databases** | `authentik` (Authentik SSO), `gitea` (Git hosting), `organizr` (Organizr dashboard), `paperless` (Document management), `system_settings` (Central Tatlock settings), `postgres` (default/admin) |
|
||||
| **Database Users** | `authentik_user`, `gitea_user`, `organizr_user`, `paperless_user`, `settings` (system_settings RW), `postgres` (superuser) |
|
||||
| **Databases** | `authentik` (Authentik SSO), `gitea` (Git hosting), `paperless` (Document management), `system_settings` (Central Tatlock settings), `postgres` (default/admin) |
|
||||
| **Database Users** | `authentik_user`, `gitea_user`, `paperless_user`, `settings` (system_settings RW), `postgres` (superuser) |
|
||||
| **Health Check** | `pg_isready -U postgres` (30s interval) |
|
||||
| **Backup Strategy** | `/backups` volume for pg_dump exports |
|
||||
|
||||
**Initialization**: Databases and users for `authentik` and `gitea` are created by the `postgres-init.sh` script.
|
||||
|
||||
**Adding Organizr Database**:
|
||||
|
||||
1. **Generate a secure password** for the `organizr_user`.
|
||||
2. **In Portainer, navigate to the `postgres-shared` service.**
|
||||
3. **Go to the "Env" tab and add a new environment variable:**
|
||||
* **Name:** `ORGANIZR_DB_PASSWORD`
|
||||
* **Value:** *Your generated password*
|
||||
4. **Redeploy the `postgres-shared` service.** This will trigger the `postgres-init.sh` script to create the `organizr` database and user.
|
||||
|
||||
---
|
||||
|
||||
### Redis Shared
|
||||
@@ -313,80 +302,20 @@ Headscale is a self-hosted control server for Tailscale's mesh VPN protocol, cre
|
||||
|
||||
---
|
||||
|
||||
## Monitoring Layer
|
||||
|
||||
### Netdata
|
||||
|
||||
Netdata provides comprehensive real-time system performance monitoring with per-second metric collection for CPU, RAM, disk I/O, network traffic, and Docker container resource usage, displaying everything through interactive web dashboards with zero configuration required. It collects thousands of metrics automatically with minimal overhead, offering drill-down capabilities from system-wide views to per-container and per-process analysis. The service maintains short-term metric history in RAM and can stream data to long-term storage backends for historical analysis.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Image** | `netdata/netdata:latest` |
|
||||
| **Container Name** | `netdata` |
|
||||
| **Access URL** | http://192.168.86.149:19999 |
|
||||
| **External Access** | LAN only (metrics dashboard) |
|
||||
| **Port Mapping** | 19999:19999 (Web UI) |
|
||||
| **Network Mode** | Host (for full system visibility) |
|
||||
| **Restart Policy** | `unless-stopped` |
|
||||
| **Volume Mounts** | `/proc:/host/proc:ro`, `/sys:/host/sys:ro`, `/var/run/docker.sock:/var/run/docker.sock:ro` |
|
||||
| **Capabilities** | `SYS_PTRACE`, `apparmor:unconfined` |
|
||||
| **Resource Limits** | None (monitoring overhead ~1-3% CPU) |
|
||||
| **GPU Required** | No |
|
||||
| **Dependencies** | Docker socket (read-only) |
|
||||
| **Metric Retention** | ~1 hour (RAM-based) |
|
||||
|
||||
---
|
||||
|
||||
### Organizr
|
||||
|
||||
Organizr serves as a comprehensive unified dashboard that consolidates all homelab services into a single tabbed interface with integrated homepage widgets showing real-time statistics from Jellyfin streams, Netdata metrics, and download client activity. It provides customizable authentication per-tab with support for SSO integration, user management with group-based access control, and a mobile-responsive interface for managing the entire infrastructure from anywhere. The service acts as a central hub replacing the need for multiple bookmarks or remembering service ports, offering both quick-access tabs and homepage cards with live data feeds from connected services.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Image** | `organizr/organizr:latest` |
|
||||
| **Container Name** | `organizr` |
|
||||
| **Access URL (LAN)** | http://192.168.86.149:9999 |
|
||||
| **Access URL (Public)** | https://home.schweitz.net |
|
||||
| **External Access** | Yes (via NPM reverse proxy with SSL) |
|
||||
| **Port Mapping** | 9999:80 (HTTP), 443:443 (HTTPS) |
|
||||
| **Network Mode** | Bridge |
|
||||
| **Restart Policy** | `unless-stopped` |
|
||||
| **Volume Mounts** | `~/docker-data/organizr:/config` |
|
||||
| **Environment** | `DB_TYPE=pgsql`, `DB_HOST=postgres-shared`, `DB_PORT=5432`, `DB_NAME=organizr`, `DB_USER=organizr_user`, `DB_PASS=${ORGANIZR_DB_PASSWORD}` |
|
||||
| **Resource Limits** | None |
|
||||
| **GPU Required** | No |
|
||||
| **Dependencies** | PostgreSQL Shared |
|
||||
| **Database** | PostgreSQL on `postgres-shared` (database `organizr`) |
|
||||
| **Database Size** | ~5-10MB (typical) |
|
||||
| **Integrated Services** | Jellyfin, Netdata |
|
||||
| **Authentication** | Internal (supports SSO, Plex OAuth, LDAP) |
|
||||
|
||||
**Configuration**:
|
||||
|
||||
1. **In Portainer, navigate to the `organizr` stack.**
|
||||
2. **Go to the "Env" tab and ensure the following environment variables are set:**
|
||||
* `DB_TYPE=pgsql`
|
||||
* `DB_HOST=postgres-shared`
|
||||
* `DB_PORT=5432`
|
||||
* `DB_NAME=organizr`
|
||||
* `DB_USER=organizr_user`
|
||||
* `DB_PASS`: This should be a secret. Create a secret in Portainer named `ORGANIZR_DB_PASSWORD` and set its value to the password you generated for the `organizr_user`.
|
||||
3. **Redeploy the `organizr` stack.**
|
||||
|
||||
---
|
||||
## Dashboard Layer
|
||||
|
||||
### Tatlock UI
|
||||
|
||||
Tatlock UI is a modern Flutter-based home lab dashboard designed to replace Organizr, providing a responsive web interface for monitoring and accessing all infrastructure services. Built as a stateless static web application served via nginx, it offers fast load times and a clean, customizable interface for the tower-of-joy infrastructure. The Flutter web build is compiled and containerized via Gitea Actions, with automatic deployment through Watchtower.
|
||||
Tatlock UI is a modern Flutter-based home lab dashboard providing a responsive web interface for monitoring and accessing all infrastructure services. Built as a stateless static web application served via nginx, it offers fast load times and a clean, customizable interface for the tower-of-joy infrastructure. The Flutter web build is compiled and containerized via Gitea Actions, with automatic deployment through Watchtower.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Image** | `git.schweitz.net/jpmschweitzer/tatlock-ui:latest` |
|
||||
| **Container Name** | `tatlock-ui` |
|
||||
| **Access URL (LAN)** | http://192.168.86.149:8092 |
|
||||
| **Access URL (Public)** | https://home.schweitz.net (future, replacing Organizr) |
|
||||
| **External Access** | LAN only (future: via NPM reverse proxy with SSL) |
|
||||
| **Port Mapping** | 8092:80 (HTTP) |
|
||||
| **Access URL (LAN)** | http://192.168.86.149:9999 |
|
||||
| **Access URL (Public)** | https://home.schweitz.net |
|
||||
| **External Access** | Yes (via NPM reverse proxy with SSL) |
|
||||
| **Port Mapping** | 9999:80 (HTTP) |
|
||||
| **Network Mode** | Bridge (docker-dataplane) |
|
||||
| **Restart Policy** | `unless-stopped` |
|
||||
| **Volume Mounts** | None (stateless static web app) |
|
||||
@@ -675,8 +604,6 @@ Gitea is a lightweight, self-hosted Git service providing repository hosting, is
|
||||
| **Code-Server** | https://code.schweitz.net | Yes | Browser-based IDE |
|
||||
| **Ollama** | http://192.168.86.149:11434 | No | ML model API |
|
||||
| **Headscale** | http://192.168.86.149:8085 | No | VPN control server |
|
||||
| **Netdata** | http://192.168.86.149:19999 | No | System metrics |
|
||||
| **Organizr** | https://home.schweitz.net | Yes | Unified dashboard |
|
||||
| **Open WebUI** | https://webui.schweitz.net | Yes (SSO) | LLM chat interface |
|
||||
| **Tatlock** | https://tatlock.schweitz.net | Yes (SSO) | AI orchestration API |
|
||||
| **Core API** | http://192.168.86.149:8083 | No | API functions & infrastructure mgmt |
|
||||
@@ -685,7 +612,7 @@ Gitea is a lightweight, self-hosted Git service providing repository hosting, is
|
||||
| **Gitea** | https://git.schweitz.net | Yes | Git repository hosting |
|
||||
| **Samba** | \\\\192.168.86.149 | No | Network file shares |
|
||||
| **Home Assistant** | https://housekeeping.schweitz.net | Yes | Smart home automation |
|
||||
| **Tatlock UI** | http://192.168.86.149:8092 | No (future: Yes) | Home lab dashboard |
|
||||
| **Tatlock UI** | https://home.schweitz.net | Yes | Home lab dashboard |
|
||||
| **Watchtower** | N/A (background) | N/A | Auto-updates |
|
||||
|
||||
---
|
||||
@@ -712,8 +639,6 @@ Gitea is a lightweight, self-hosted Git service providing repository hosting, is
|
||||
| **Code-Server** | `~/.config/code-server/`, `~/docker-data/code-server/` | N/A | Config: ~5MB, Extensions: ~50-200MB, User data: ~50MB |
|
||||
| **Ollama** | `~/docker-data/ollama/models/` | Alt: `/mnt/media/ollama/` | 2-15GB per model |
|
||||
| **Headscale** | `~/docker-data/headscale/` | N/A | ~10MB |
|
||||
| **Netdata** | RAM-based (ephemeral) | N/A | ~200MB RAM |
|
||||
| **Organizr** | `~/docker-data/organizr/` | N/A | ~50MB |
|
||||
| **Open WebUI** | `~/docker-data/open-webui/` | N/A | ~100MB |
|
||||
| **Core API** | `~/docker-data/core-api/` | N/A | Logs: ~10MB |
|
||||
| **Jellyfin** | `~/docker-data/jellyfin/` | `/mnt/media/jellyfin/` | Config: ~500MB, Media: ~2TB |
|
||||
@@ -734,7 +659,7 @@ Gitea is a lightweight, self-hosted Git service providing repository hosting, is
|
||||
|
||||
| Network Name | Containers | Purpose |
|
||||
|--------------|------------|---------|
|
||||
| **docker-dataplane** | Ollama, Open WebUI, Core API, Qdrant, PostgreSQL Shared, Redis Shared, Headscale, Nextcloud, Gitea, Samba, Watchtower, Organizr, Netdata, Home Assistant | Unified service mesh for all containerized applications |
|
||||
| **docker-dataplane** | Ollama, Open WebUI, Core API, Qdrant, PostgreSQL Shared, Redis Shared, Headscale, Nextcloud, Gitea, Samba, Watchtower, Tatlock UI, Home Assistant | Unified service mesh for all containerized applications |
|
||||
| **host** | Portainer, NPM | Direct host port access for infrastructure management |
|
||||
|
||||
**Benefits of Consolidation**:
|
||||
|
||||
@@ -23,7 +23,7 @@ help:
|
||||
@echo ""
|
||||
@echo "Available Stacks:"
|
||||
@echo " - portainer, nginx-proxy-manager, ollama"
|
||||
@echo " - headscale, netdata"
|
||||
@echo " - headscale"
|
||||
@echo " - watchtower, maintenance"
|
||||
@echo " - jellyfin, nextcloud, samba"
|
||||
@echo ""
|
||||
@@ -109,7 +109,7 @@ update-%:
|
||||
# Setup directories
|
||||
setup-dirs:
|
||||
@echo "Creating directory structure..."
|
||||
@mkdir -p ~/docker-data/{portainer,nginx-proxy-manager,ollama,headscale,netdata,jellyfin,nextcloud,samba}
|
||||
@mkdir -p ~/docker-data/{portainer,nginx-proxy-manager,ollama,headscale,jellyfin,nextcloud,samba}
|
||||
@mkdir -p /mnt/media/{jellyfin,nextcloud,game-servers,backups,downloads}
|
||||
@echo "✅ Directories created"
|
||||
@echo ""
|
||||
@@ -164,21 +164,9 @@ deploy-phase2:
|
||||
@echo " 2. Create user: docker exec headscale headscale users create homelab"
|
||||
@echo " 3. Generate key: docker exec headscale headscale preauthkeys create --user homelab"
|
||||
|
||||
# Quick deploy - Phase 3 monitoring
|
||||
# Quick deploy - Phase 3 optimization
|
||||
deploy-phase3:
|
||||
@echo "=== Deploying Phase 3: Monitoring ==="
|
||||
@echo ""
|
||||
@echo "Deploying Netdata..."
|
||||
@make deploy-netdata
|
||||
@echo ""
|
||||
@echo "=== Phase 3 Complete ==="
|
||||
@echo ""
|
||||
@echo "Access monitoring:"
|
||||
@echo " Netdata: http://localhost:19999"
|
||||
|
||||
# Quick deploy - Phase 4 optimization
|
||||
deploy-phase4:
|
||||
@echo "=== Deploying Phase 4: Optimization ==="
|
||||
@echo "=== Deploying Phase 3: Optimization ==="
|
||||
@echo ""
|
||||
@echo "[1/2] Deploying Watchtower..."
|
||||
@make deploy-watchtower
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> Self-hosted home server infrastructure with GPU-accelerated ML, media streaming, and secure remote access
|
||||
|
||||
**Main Dashboard:** https://home.schweitz.net (Organizr)
|
||||
**Main Dashboard:** https://home.schweitz.net (Tatlock UI)
|
||||
|
||||
## Quick Links
|
||||
|
||||
@@ -34,9 +34,8 @@
|
||||
│ ├── docker-dataplane - Service mesh │
|
||||
│ └── Headscale (8085) - VPN mesh │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Monitoring Layer │
|
||||
│ ├── Netdata (19999) - Metrics │
|
||||
│ └── Organizr (8084) - Dashboard │
|
||||
│ Dashboard Layer │
|
||||
│ └── Tatlock UI (9999) - Dashboard │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Optimization Layer │
|
||||
│ ├── Watchtower - Auto-updates │
|
||||
@@ -69,7 +68,6 @@ portainer-core/
|
||||
├── services/ # Service source code
|
||||
│ ├── core-api/ # Infrastructure management API
|
||||
│ └── ...
|
||||
├── organizr-widgets/ # Dashboard widgets
|
||||
├── AGENTS.md # AI agent guidelines (single source of truth)
|
||||
├── README.md # This file (documentation index)
|
||||
├── PLANS.md # Implementation plan tracker
|
||||
@@ -130,7 +128,7 @@ See [External Services](docs/EXTERNAL_SERVICES.md) for services maintained in se
|
||||
| **Open WebUI** | 8081 | LLM chat interface |
|
||||
| **Nextcloud** | 8082 | Cloud storage |
|
||||
| **Core API** | 8083 | Infrastructure management API |
|
||||
| **Organizr** | 8084 | Unified dashboard |
|
||||
| **Tatlock UI** | 9999 | Home lab dashboard |
|
||||
| **Headscale** | 8085 | VPN control server |
|
||||
| **Jellyfin** | 8096 | Media streaming |
|
||||
| **Gitea** | 3002 | Git repository hosting |
|
||||
@@ -139,8 +137,6 @@ See [External Services](docs/EXTERNAL_SERVICES.md) for services maintained in se
|
||||
| **Redis Shared** | 6379 | Shared cache (internal) |
|
||||
| **Qdrant** | 6333, 6334 | Vector database |
|
||||
| **Ollama** | 11434 | ML model API |
|
||||
| **Netdata** | 19999 | System monitoring |
|
||||
|
||||
See [Stacks Reference](docs/reference/stacks.md#port-allocation) for complete port allocation.
|
||||
|
||||
## GPU Services
|
||||
|
||||
+2
-23
@@ -2,35 +2,14 @@
|
||||
|
||||
This directory contains Nginx configuration snippets for Nginx Proxy Manager (NPM) forward authentication with Authentik.
|
||||
|
||||
## Files
|
||||
|
||||
### `organizr-forward-auth.conf`
|
||||
**Status:** 🧪 Testing
|
||||
**Service:** Organizr (home.schweitz.net)
|
||||
**Purpose:** First test deployment of forward auth to validate standalone outpost functionality
|
||||
|
||||
**DO NOT APPLY TO OTHER SERVICES YET** - This is a proof-of-concept deployment to verify:
|
||||
- Standalone outpost works correctly
|
||||
- No redirect loops occur
|
||||
- SSO functions as expected
|
||||
- Cookie domain settings are correct
|
||||
|
||||
Once proven stable, this configuration can be adapted for other services.
|
||||
|
||||
## Deployment Strategy
|
||||
|
||||
### Phase 1: Single Service Test (Current)
|
||||
- ✅ Deploy to Organizr only
|
||||
- ✅ Test all authentication flows
|
||||
- ✅ Verify no issues for 24-48 hours
|
||||
|
||||
### Phase 2: Gradual Rollout (After Phase 1 Success)
|
||||
Services to protect (in order):
|
||||
Services to protect with forward auth (in order):
|
||||
1. Core API (api.schweitz.net) - Use OIDC instead of forward auth
|
||||
2. Nextcloud (cloud.schweitz.net)
|
||||
3. Gitea (git.schweitz.net)
|
||||
4. Jellyfin (media.schweitz.net)
|
||||
5. Open WebUI, Netdata, etc.
|
||||
5. Open WebUI, etc.
|
||||
|
||||
**Rule:** Deploy to ONE service at a time, test for 24 hours before proceeding to next.
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# NPM Forward Auth Configuration for Organizr (home.schweitz.net)
|
||||
# Test deployment - single service only
|
||||
# Date: 2025-11-21
|
||||
# Authentik Version: 2024.8.4
|
||||
# Standalone Outpost: authentik-proxy (port 9445)
|
||||
|
||||
# ===================================================================
|
||||
# IMPORTANT: Apply this ONLY to home.schweitz.net proxy host
|
||||
# DO NOT apply to other services until this is proven stable
|
||||
# ===================================================================
|
||||
|
||||
# Increase buffer size for large headers from Authentik
|
||||
proxy_buffers 8 16k;
|
||||
proxy_buffer_size 32k;
|
||||
|
||||
# Forward authentication via standalone outpost
|
||||
auth_request /outpost.goauthentik.io/auth/nginx;
|
||||
error_page 401 = @goauthentik_proxy_signin;
|
||||
|
||||
# Capture auth response headers
|
||||
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||
auth_request_set $authentik_username $upstream_http_x_authentik_username;
|
||||
auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
|
||||
auth_request_set $authentik_email $upstream_http_x_authentik_email;
|
||||
auth_request_set $authentik_name $upstream_http_x_authentik_name;
|
||||
auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
|
||||
|
||||
# Forward auth headers to application
|
||||
add_header Set-Cookie $auth_cookie;
|
||||
proxy_set_header X-authentik-username $authentik_username;
|
||||
proxy_set_header X-authentik-groups $authentik_groups;
|
||||
proxy_set_header X-authentik-email $authentik_email;
|
||||
proxy_set_header X-authentik-name $authentik_name;
|
||||
proxy_set_header X-authentik-uid $authentik_uid;
|
||||
|
||||
# Outpost proxy location
|
||||
location /outpost.goauthentik.io {
|
||||
proxy_pass https://localhost:9445/outpost.goauthentik.io;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
# Signin redirect handler
|
||||
location @goauthentik_proxy_signin {
|
||||
internal;
|
||||
return 302 https://auth.schweitz.net/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# DEPLOYMENT INSTRUCTIONS:
|
||||
# ===================================================================
|
||||
#
|
||||
# 1. Open NPM UI: http://192.168.86.149:8000
|
||||
# 2. Navigate to: Hosts → Proxy Hosts
|
||||
# 3. Find "home.schweitz.net" and click Edit
|
||||
# 4. Go to the "Advanced" tab
|
||||
# 5. PASTE THIS ENTIRE CONFIGURATION (lines 11-56) into the text box
|
||||
# 6. Go to the "SSL" tab
|
||||
# 7. Ensure "WebSockets Support" is ENABLED
|
||||
# 8. Click "Save"
|
||||
#
|
||||
# ===================================================================
|
||||
# TESTING PROCEDURE:
|
||||
# ===================================================================
|
||||
#
|
||||
# Step 1: Test in Incognito Window
|
||||
# - Open incognito/private browsing window
|
||||
# - Navigate to: https://home.schweitz.net
|
||||
# - Expected: Redirect to https://auth.schweitz.net
|
||||
# - Login with Google OAuth
|
||||
# - Expected: Redirect back to https://home.schweitz.net
|
||||
# - Expected: Organizr loads successfully
|
||||
#
|
||||
# Step 2: Verify SSO Persistence
|
||||
# - Close incognito window
|
||||
# - Open new incognito window
|
||||
# - Navigate to: https://home.schweitz.net
|
||||
# - Expected: Still logged in (cookie persists)
|
||||
#
|
||||
# Step 3: Check Logs for Errors
|
||||
# docker logs authentik-proxy 2>&1 | tail -50
|
||||
# - Look for any errors or warnings
|
||||
# - Should see successful auth requests
|
||||
#
|
||||
# Step 4: Test Logout
|
||||
# - Navigate to: https://auth.schweitz.net/if/flow/default-invalidation-flow/
|
||||
# - Should log out
|
||||
# - Try accessing https://home.schweitz.net again
|
||||
# - Expected: Redirect to login page
|
||||
#
|
||||
# ===================================================================
|
||||
# ROLLBACK PROCEDURE (if issues occur):
|
||||
# ===================================================================
|
||||
#
|
||||
# 1. Open NPM UI
|
||||
# 2. Edit home.schweitz.net proxy host
|
||||
# 3. Go to "Advanced" tab
|
||||
# 4. DELETE all the configuration
|
||||
# 5. Save
|
||||
# 6. Organizr will be accessible without authentication again
|
||||
#
|
||||
# ===================================================================
|
||||
# TROUBLESHOOTING:
|
||||
# ===================================================================
|
||||
#
|
||||
# Issue: Redirect loop
|
||||
# - Check that auth.schweitz.net does NOT have forward auth enabled
|
||||
# - Verify AUTHENTIK_COOKIE_DOMAIN=.schweitz.net in provider settings
|
||||
#
|
||||
# Issue: 502 Bad Gateway
|
||||
# - Check authentik-proxy container is running: docker ps | grep authentik-proxy
|
||||
# - Check NPM can reach authentik-proxy: docker exec npm ping authentik-proxy
|
||||
#
|
||||
# Issue: 500 Internal Server Error
|
||||
# - Check authentik-proxy logs: docker logs authentik-proxy
|
||||
# - Verify Redis connection is working
|
||||
# - Restart authentik-proxy: docker restart authentik-proxy
|
||||
#
|
||||
# Issue: Authentication works but Organizr doesn't load
|
||||
# - Check buffer sizes are set correctly (lines 13-14)
|
||||
# - Check WebSocket support is enabled in NPM SSL tab
|
||||
#
|
||||
# ===================================================================
|
||||
@@ -1,224 +0,0 @@
|
||||
# Organizr Service Control Widget
|
||||
|
||||
A beautiful, responsive widget for managing on-demand services from your Organizr dashboard.
|
||||
|
||||
## Features
|
||||
|
||||
- ✨ **Real-time Status** - Live service status with container counts
|
||||
- 🎮 **One-Click Control** - Start/Stop services with a single click
|
||||
- 🔒 **Safety First** - Always-on services are protected and clearly marked
|
||||
- 🎨 **Beautiful UI** - Dark theme that matches Organizr
|
||||
- ⚡ **Auto-Refresh** - Updates every 10 seconds
|
||||
- 📱 **Responsive** - Works on desktop, tablet, and mobile
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Service Cards
|
||||
Each service shows:
|
||||
- Service name
|
||||
- Running status (Running/Stopped with container counts)
|
||||
- Start/Stop buttons (disabled when not applicable)
|
||||
- "ALWAYS ON" badge for infrastructure services
|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: Organizr Custom Homepage Item (Recommended)
|
||||
|
||||
1. **Copy the widget file** to a web-accessible location:
|
||||
```bash
|
||||
# If you have a web server serving files from /var/www/html:
|
||||
sudo cp service-control.html /var/www/html/widgets/
|
||||
|
||||
# Or use Organizr's public directory:
|
||||
cp service-control.html /path/to/organizr/plugins/widgets/
|
||||
```
|
||||
|
||||
2. **Add to Organizr Homepage**:
|
||||
- Open Organizr
|
||||
- Go to **Settings** → **Customize** → **Homepage Items**
|
||||
- Click **Add New Item**
|
||||
- Configure:
|
||||
- **Name**: "Service Control"
|
||||
- **Category**: Custom
|
||||
- **Type**: iFrame
|
||||
- **URL**: `http://localhost/widgets/service-control.html` (adjust path)
|
||||
- **Minimum Authentication**: User
|
||||
- **Enabled**: Yes
|
||||
- Save
|
||||
|
||||
3. **Add to Homepage**:
|
||||
- Go to **Settings** → **Customize** → **Appearance**
|
||||
- Edit your homepage layout
|
||||
- Add the "Service Control" item to desired location
|
||||
- Save
|
||||
|
||||
### Method 2: Organizr Custom HTML Tab
|
||||
|
||||
1. **Open Organizr Settings**:
|
||||
- Settings → **Tab Editor**
|
||||
|
||||
2. **Add New Tab**:
|
||||
- Click **Add Tab**
|
||||
- Configure:
|
||||
- **Tab Name**: "Services"
|
||||
- **Tab URL**: Leave empty
|
||||
- **Category**: Custom
|
||||
- **Type**: iFrame
|
||||
- **Image**: `images/tabs/services.png` (or your choice)
|
||||
|
||||
3. **Add Custom HTML**:
|
||||
- In the same tab configuration, find **Custom HTML** section
|
||||
- Copy and paste the entire contents of `service-control.html`
|
||||
- Save
|
||||
|
||||
4. **Access the Tab**:
|
||||
- The "Services" tab will now appear in your Organizr sidebar
|
||||
|
||||
### Method 3: Nginx Reverse Proxy Integration
|
||||
|
||||
If you want to serve the widget through Nginx Proxy Manager:
|
||||
|
||||
1. **Create a location** in your Organizr proxy host:
|
||||
```nginx
|
||||
location /widgets/ {
|
||||
alias /path/to/portainer-core/organizr-widgets/;
|
||||
autoindex off;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Access via**: `https://your-organizr-domain.com/widgets/service-control.html`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Changing API Endpoint
|
||||
|
||||
If your core-api is not on `localhost:8083`, edit the widget file:
|
||||
|
||||
```javascript
|
||||
const API_BASE = 'http://your-server:8083'; // Change this line
|
||||
```
|
||||
|
||||
### Adjusting Auto-Refresh Interval
|
||||
|
||||
Default is 10 seconds. To change:
|
||||
|
||||
```javascript
|
||||
setInterval(fetchServices, 10000); // Change 10000 to desired milliseconds
|
||||
```
|
||||
|
||||
### Customizing Displayed Services
|
||||
|
||||
By default, the widget shows all stoppable services (excludes always-on infrastructure).
|
||||
|
||||
To filter specific services, modify the `renderServices()` function:
|
||||
|
||||
```javascript
|
||||
const stoppableServices = services.filter(s =>
|
||||
!isAlwaysOn(s.name) &&
|
||||
['jellyfin', 'nextcloud', 'gitea', 'ai-stack'].includes(s.name) // Add this line
|
||||
);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Failed to connect to API"
|
||||
|
||||
**Problem**: Widget shows red error message
|
||||
|
||||
**Solutions**:
|
||||
1. Verify core-api is running: `docker ps | grep core-api`
|
||||
2. Check core-api URL is correct (localhost vs IP address)
|
||||
3. If accessing from remote, change `API_BASE` to full URL
|
||||
4. Check browser console for CORS errors
|
||||
|
||||
### CORS Issues
|
||||
|
||||
If accessing widget from a different domain than core-api:
|
||||
|
||||
**Option 1**: Update core-api CORS settings in `src/config.py`:
|
||||
```python
|
||||
cors_origins: list[str] = ["http://your-organizr-domain.com"]
|
||||
```
|
||||
|
||||
**Option 2**: Proxy the API through same domain using Nginx
|
||||
|
||||
### Services Not Appearing
|
||||
|
||||
**Check**:
|
||||
1. Services are deployed as Portainer stacks
|
||||
2. Services have proper labels: `com.docker.compose.project`
|
||||
3. Core-API can connect to Portainer
|
||||
4. Check browser console for errors
|
||||
|
||||
### Buttons Disabled
|
||||
|
||||
**Expected Behavior**:
|
||||
- Start button disabled when service is running
|
||||
- Stop button disabled when service is stopped
|
||||
- All buttons disabled for always-on services
|
||||
|
||||
## Service Groups
|
||||
|
||||
The following service groups are defined (stopping one stops all in group):
|
||||
|
||||
- **jellyfin**: jellyfin
|
||||
- **nextcloud**: nextcloud (uses shared postgres-shared + redis-shared)
|
||||
- **gitea**: gitea, gitea-db
|
||||
- **ai-stack**: open-webui, ollama, qdrant
|
||||
- **samba**: samba
|
||||
|
||||
## Always-On Services (Cannot be stopped)
|
||||
|
||||
These infrastructure services are protected:
|
||||
- portainer
|
||||
- nginx-proxy-manager
|
||||
- core-api
|
||||
- organizr
|
||||
- headscale
|
||||
- watchtower
|
||||
- netdata
|
||||
- maintenance
|
||||
- postgres-shared (shared database infrastructure)
|
||||
- redis-shared (shared cache infrastructure)
|
||||
|
||||
## Advanced: Customizing the UI
|
||||
|
||||
### Colors
|
||||
|
||||
Edit the CSS variables in the `<style>` section:
|
||||
|
||||
```css
|
||||
.status-running {
|
||||
background: rgba(72, 187, 120, 0.2); /* Green background */
|
||||
color: #48bb78; /* Green text */
|
||||
}
|
||||
```
|
||||
|
||||
### Card Size
|
||||
|
||||
Adjust grid columns:
|
||||
|
||||
```css
|
||||
.service-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
/* Change 300px to make cards wider/narrower */
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
- `GET /infrastructure/services` - Fetch service list with status
|
||||
- `GET /infrastructure/service-groups` - Fetch service groups and always-on list
|
||||
- `POST /infrastructure/services/{name}/start` - Start a service
|
||||
- `POST /infrastructure/services/{name}/stop` - Stop a service
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the core-api logs: `docker logs core-api`
|
||||
2. Check browser console for JavaScript errors
|
||||
3. Verify API endpoints work: `curl http://localhost:8083/infrastructure/services`
|
||||
|
||||
## License
|
||||
|
||||
Part of the portainer-core project.
|
||||
@@ -1,342 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Service Control</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: transparent;
|
||||
color: #e0e0e0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: rgba(66, 153, 225, 0.5);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: rgba(72, 187, 120, 0.2);
|
||||
color: #48bb78;
|
||||
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background: rgba(245, 101, 101, 0.2);
|
||||
color: #f56565;
|
||||
border: 1px solid rgba(245, 101, 101, 0.4);
|
||||
}
|
||||
|
||||
.status-loading {
|
||||
background: rgba(237, 137, 54, 0.2);
|
||||
color: #ed8936;
|
||||
border: 1px solid rgba(237, 137, 54, 0.4);
|
||||
}
|
||||
|
||||
.service-info {
|
||||
font-size: 13px;
|
||||
color: #a0a0a0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.service-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-start:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-stop {
|
||||
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-stop:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-restart {
|
||||
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-restart:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(245, 101, 101, 0.1);
|
||||
border: 1px solid rgba(245, 101, 101, 0.4);
|
||||
color: #f56565;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.always-on-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: rgba(66, 153, 225, 0.2);
|
||||
color: #4299e1;
|
||||
border: 1px solid rgba(66, 153, 225, 0.4);
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>🎛️ On-Demand Services</h2>
|
||||
<div id="error-container"></div>
|
||||
<div id="service-container" class="loading">Loading services...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8083';
|
||||
let services = [];
|
||||
let alwaysOnServices = [];
|
||||
|
||||
async function fetchServices() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/infrastructure/services`);
|
||||
if (!response.ok) throw new Error('Failed to fetch services');
|
||||
services = await response.json();
|
||||
|
||||
const groupsResponse = await fetch(`${API_BASE}/infrastructure/service-groups`);
|
||||
if (groupsResponse.ok) {
|
||||
const groupsData = await groupsResponse.json();
|
||||
alwaysOnServices = groupsData.always_on || [];
|
||||
}
|
||||
|
||||
renderServices();
|
||||
document.getElementById('error-container').innerHTML = '';
|
||||
} catch (error) {
|
||||
console.error('Error fetching services:', error);
|
||||
document.getElementById('error-container').innerHTML =
|
||||
`<div class="error">❌ Failed to connect to API: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function isAlwaysOn(serviceName) {
|
||||
return alwaysOnServices.includes(serviceName.toLowerCase());
|
||||
}
|
||||
|
||||
function getServiceStatus(service) {
|
||||
if (service.containers_running > 0) {
|
||||
return {
|
||||
class: 'status-running',
|
||||
text: `Running (${service.containers_running}/${service.containers_total})`
|
||||
};
|
||||
} else if (service.containers_total > 0) {
|
||||
return {
|
||||
class: 'status-stopped',
|
||||
text: 'Stopped'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
class: 'status-stopped',
|
||||
text: 'No containers'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function renderServices() {
|
||||
const container = document.getElementById('service-container');
|
||||
|
||||
// Filter to only show stoppable services
|
||||
const stoppableServices = services.filter(s => !isAlwaysOn(s.name));
|
||||
|
||||
if (stoppableServices.length === 0) {
|
||||
container.innerHTML = '<div class="loading">No stoppable services found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.className = 'service-grid';
|
||||
container.innerHTML = stoppableServices.map(service => {
|
||||
const status = getServiceStatus(service);
|
||||
const isRunning = service.containers_running > 0;
|
||||
const alwaysOn = isAlwaysOn(service.name);
|
||||
|
||||
return `
|
||||
<div class="service-card" data-service="${service.name}">
|
||||
<div class="service-header">
|
||||
<span class="service-name">
|
||||
${service.name}
|
||||
${alwaysOn ? '<span class="always-on-badge">ALWAYS ON</span>' : ''}
|
||||
</span>
|
||||
<span class="status-badge ${status.class}">${status.text}</span>
|
||||
</div>
|
||||
<div class="service-info">
|
||||
Stack ID: ${service.stack_id || 'N/A'}
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="btn btn-start"
|
||||
onclick="controlService('${service.name}', 'start')"
|
||||
${isRunning || alwaysOn ? 'disabled' : ''}>
|
||||
Start
|
||||
</button>
|
||||
<button class="btn btn-stop"
|
||||
onclick="controlService('${service.name}', 'stop')"
|
||||
${!isRunning || alwaysOn ? 'disabled' : ''}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function controlService(serviceName, action) {
|
||||
const card = document.querySelector(`[data-service="${serviceName}"]`);
|
||||
const buttons = card.querySelectorAll('button');
|
||||
|
||||
// Disable all buttons and show loading
|
||||
buttons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
if (btn.textContent.toLowerCase().includes(action)) {
|
||||
btn.innerHTML = `<span class="spinner"></span>${action.toUpperCase()}...`;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/infrastructure/services/${serviceName}/${action}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || result.detail || 'Operation failed');
|
||||
}
|
||||
|
||||
console.log(`${action} ${serviceName}:`, result);
|
||||
|
||||
// Wait a bit for containers to start/stop
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Refresh service list
|
||||
await fetchServices();
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error ${action}ing ${serviceName}:`, error);
|
||||
alert(`Failed to ${action} ${serviceName}: ${error.message}`);
|
||||
|
||||
// Re-enable buttons on error
|
||||
await fetchServices();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh every 10 seconds
|
||||
setInterval(fetchServices, 10000);
|
||||
|
||||
// Initial load
|
||||
fetchServices();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -228,7 +228,6 @@ If already logged into another `.schweitz.net` service:
|
||||
|------|---------|
|
||||
| `stacks/authentik.yml` | Authentik server, worker, and standalone outpost |
|
||||
| `stacks/wiki.yml` | Example: Wiki.js container configuration |
|
||||
| `npm-configs/organizr-forward-auth.conf` | Legacy reference (has bugs, do not use directly) |
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
|
||||
+2
-9
@@ -22,13 +22,7 @@ review the http://core-api/docs openapi documentation for infrastructure managem
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Headscale** | `headscale.yml` | 8085, 9090 | No | Self-hosted Tailscale control server |
|
||||
|
||||
### Phase 3: Monitoring
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Netdata** | `netdata.yml` | 19999 | No | Real-time system performance monitoring |
|
||||
|
||||
### Phase 4: Optimization
|
||||
### Phase 3: Optimization
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
@@ -62,9 +56,8 @@ review the http://core-api/docs openapi documentation for infrastructure managem
|
||||
- 2222: Gitea SSH
|
||||
- 3002: Gitea HTTP
|
||||
|
||||
### Monitoring Services (19000-19999)
|
||||
### Backup Services
|
||||
- 8200: Duplicati
|
||||
- 19999: Netdata
|
||||
|
||||
### ML/API Services (11000+)
|
||||
- 11434: Ollama
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
# Netdata - Real-Time System Performance Monitoring
|
||||
# Phase 3: Monitoring & Management
|
||||
# Ports: 19999
|
||||
# GPU: No
|
||||
# Storage: Minimal (reads from host /proc and /sys)
|
||||
|
||||
services:
|
||||
netdata:
|
||||
image: netdata/netdata:latest
|
||||
container_name: netdata
|
||||
restart: unless-stopped
|
||||
hostname: tower-of-joy
|
||||
ports:
|
||||
- "19999:19999"
|
||||
cap_add:
|
||||
- SYS_PTRACE # Required for process monitoring
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
# Optional: Claim to Netdata Cloud for remote access
|
||||
# - NETDATA_CLAIM_TOKEN=your-claim-token
|
||||
# - NETDATA_CLAIM_URL=https://app.netdata.cloud
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fSs http://localhost:19999/api/v1/info || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- docker-dataplane
|
||||
|
||||
networks:
|
||||
docker-dataplane:
|
||||
external: true
|
||||
name: docker-dataplane
|
||||
|
||||
# After Deployment:
|
||||
# 1. Access http://localhost:19999
|
||||
# 2. Explore dashboard sections:
|
||||
# - CPU usage
|
||||
# - RAM usage
|
||||
# - Disk I/O
|
||||
# - Network traffic
|
||||
# - Docker containers
|
||||
# - GPU monitoring (if configured)
|
||||
# 3. Set up alerts (optional)
|
||||
# 4. Configure alarm notifications
|
||||
#
|
||||
# Features:
|
||||
# - Real-time graphs (1s granularity)
|
||||
# - Per-container metrics
|
||||
# - System health monitoring
|
||||
# - Historical data
|
||||
# - Low resource overhead
|
||||
@@ -1,79 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
# Organizr - Unified Dashboard with Tabbed Interface
|
||||
# "One page to rule them all" - Tabs for all services in single interface
|
||||
# Phase 3.5: Unified Access Layer
|
||||
# Ports: 9999 (Web UI)
|
||||
# GPU: No
|
||||
# Storage: SSD (configs and user data)
|
||||
|
||||
services:
|
||||
organizr:
|
||||
image: organizr/organizr:latest
|
||||
container_name: organizr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9999:80"
|
||||
volumes:
|
||||
# SSD storage for configs and user data
|
||||
- /home/jpmschweitzer/docker-data/organizr:/config
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Amsterdam
|
||||
- fpm=true # Enable PHP-FPM for better performance
|
||||
- DB_TYPE=pgsql
|
||||
- DB_HOST=postgres-shared
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=organizr
|
||||
- DB_USER=organizr_user
|
||||
- DB_PASS=${ORGANIZR_DB_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fSs http://localhost:80/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- docker-dataplane
|
||||
|
||||
networks:
|
||||
docker-dataplane:
|
||||
external: true
|
||||
name: docker-dataplane
|
||||
|
||||
# Setup Instructions:
|
||||
# 1. Ensure tower-of-joy is connected to Headscale mesh (get mesh IP)
|
||||
# 2. Deploy this stack: make deploy-organizr
|
||||
# 3. Access via mesh IP: http://10.99.0.1:9999 (or local: http://192.168.86.149:9999)
|
||||
# 4. Complete setup wizard
|
||||
# 5. Add tabs using MESH IPs for VPN access:
|
||||
# - Portainer: http://10.99.0.1:8001
|
||||
# - Netdata: http://10.99.0.1:19999
|
||||
# - Jellyfin: http://10.99.0.1:8096 (when deployed)
|
||||
# - Nextcloud: http://10.99.0.1:8082 (when deployed)
|
||||
#
|
||||
# Access Pattern (Option B - Hybrid):
|
||||
# VPN Access (Primary - Admin Tools):
|
||||
# - Connect to Headscale VPN
|
||||
# - Access Organizr: http://10.99.0.1:9999
|
||||
# - All tabs use mesh IPs (10.99.0.x)
|
||||
# - Secure, no public exposure
|
||||
#
|
||||
# Public Access (Media/Files Only):
|
||||
# - https://home.schweitz.net → Organizr (optional public)
|
||||
# - https://media.schweitz.net → Jellyfin
|
||||
# - https://cloud.schweitz.net → Nextcloud
|
||||
# - Admin tools NOT accessible without VPN
|
||||
#
|
||||
# Mesh IP Benefits:
|
||||
# - Works from anywhere (VPN connected)
|
||||
# - No complex proxy rewrites needed
|
||||
# - Secure by default
|
||||
# - Easy device addition
|
||||
#
|
||||
# Important Notes:
|
||||
# - Use mesh IPs (10.99.0.x) instead of local IPs (192.168.86.x)
|
||||
# - Some services may block iframe embedding (X-Frame-Options header)
|
||||
# - Organizr authentication secures public access
|
||||
# - See docs/mesh-access-strategy.md for complete guide
|
||||
@@ -1,9 +1,9 @@
|
||||
version: '3.8'
|
||||
|
||||
# Tatlock UI - Home Lab Dashboard (Flutter Web)
|
||||
# Port: 8092 (HTTP)
|
||||
# Port: 9999 (HTTP)
|
||||
# GPU: No
|
||||
# External: home.schweitz.net (future, replacing Organizr)
|
||||
# External: home.schweitz.net
|
||||
# Storage: None (stateless static web app)
|
||||
#
|
||||
# Container image built from: git.schweitz.internal/jpmschweitzer/tatlock-ui
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
container_name: tatlock-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8092:80"
|
||||
- "9999:80"
|
||||
networks:
|
||||
- docker-dataplane
|
||||
labels:
|
||||
@@ -39,6 +39,6 @@ networks:
|
||||
name: docker-dataplane
|
||||
|
||||
# Access:
|
||||
# - Internal: http://tower-of-joy:8092
|
||||
# - Mesh VPN: http://10.99.0.1:8092
|
||||
# - Future: https://home.schweitz.net (after NPM switch from Organizr)
|
||||
# - Internal: http://tower-of-joy:9999
|
||||
# - Mesh VPN: http://10.99.0.1:9999
|
||||
# - External: https://home.schweitz.net
|
||||
|
||||
Reference in New Issue
Block a user