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:
2026-01-03 11:23:45 +01:00
co-authored by Claude Opus 4.5
parent d647a2e005
commit ba0aa5ef3d
13 changed files with 39 additions and 1000 deletions
+7 -9
View File
@@ -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
View File
@@ -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**:
+4 -16
View File
@@ -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
+4 -8
View File
@@ -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
View File
@@ -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.
-133
View File
@@ -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
#
# ===================================================================
-224
View File
@@ -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.
-342
View File
@@ -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>
-1
View File
@@ -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
View File
@@ -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
-61
View File
@@ -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
-79
View File
@@ -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
+6 -6
View File
@@ -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