Compare commits
18 Commits
b4f73de6eb
...
main
@@ -8,10 +8,10 @@ grep -E "set .port|server_name" /app/nginxproxy/data/nginx/proxy_host/*.conf \
|
|||||||
| sort -n \
|
| sort -n \
|
||||||
| column -t -N "PORT,TARGET,OTHER_TARGET" -W "OTHER_TARGET" -s ' ' -o ' | ';
|
| column -t -N "PORT,TARGET,OTHER_TARGET" -W "OTHER_TARGET" -s ' ' -o ' | ';
|
||||||
}
|
}
|
||||||
alias ports='_ports'
|
alias nginx='_ports'
|
||||||
|
|
||||||
# tmp rebuild for hugo development
|
# tmp rebuild for hugo development
|
||||||
alias hugo-rebuild='rm -rf public/ && hugo server --appendPort=false --baseURL="/" --ignoreCache'
|
alias hugo-rebuild='cd /srv/dev/hugo/wiki && ./preview.sh public'
|
||||||
|
|
||||||
# helpers for quickly editing aliases
|
# helpers for quickly editing aliases
|
||||||
alias src='source ~/.bash_aliases'
|
alias src='source ~/.bash_aliases'
|
||||||
@@ -61,3 +61,47 @@ function _tagger() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
alias tag='_tagger'
|
alias tag='_tagger'
|
||||||
|
|
||||||
|
# docker services
|
||||||
|
function _docker-table() {
|
||||||
|
if ! docker info > /dev/null 2>&1; then
|
||||||
|
echo "Error: Docker daemon is not running."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
# 1. Adjusted Header to match your 3 requested columns
|
||||||
|
printf "NAME|STATUS|EXPOSED PORTS\n"
|
||||||
|
|
||||||
|
# 2. Match the format string to exactly what you are reading
|
||||||
|
docker ps -a --format "{{.Names}}|{{.Status}}|{{.Ports}}" | while IFS='|' read -r name status ports; do
|
||||||
|
if [ -n "$ports" ]; then
|
||||||
|
# Split ports strictly by comma
|
||||||
|
IFS=',' read -r -a port_array <<< "$ports"
|
||||||
|
cleaned_ports=""
|
||||||
|
|
||||||
|
for p in "${port_array[@]}"; do
|
||||||
|
# Trim any leading/trailing spaces from the array element
|
||||||
|
p=$(echo "$p" | xargs)
|
||||||
|
|
||||||
|
# Strip everything up to and including '->' if it exists
|
||||||
|
cleaned_p="${p##*->}"
|
||||||
|
|
||||||
|
# Prevent duplicate listings (e.g. if IPv4 and IPv6 both map to 8080/tcp)
|
||||||
|
if [[ ", $cleaned_ports, " != *", $cleaned_p, "* ]]; then
|
||||||
|
if [ -z "$cleaned_ports" ]; then
|
||||||
|
cleaned_ports="$cleaned_p"
|
||||||
|
else
|
||||||
|
cleaned_ports="$cleaned_ports, $cleaned_p"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
ports="$cleaned_ports"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print the 3 columns cleanly
|
||||||
|
printf "%s|%s|%s\n" "$name" "$status" "$ports"
|
||||||
|
done
|
||||||
|
) | column -t -s '|'
|
||||||
|
}
|
||||||
|
alias docker-services='_docker-table'
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -40,3 +40,6 @@ dump.rdb
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Desktop Services Store
|
||||||
|
.DS_Store
|
||||||
@@ -14,6 +14,11 @@ Specialized configuration files (JSON) for [Pterodactyl Panel](https://pterodact
|
|||||||
|
|
||||||
For more details on how to import and use these, see the [Eggs README](./eggs/README.md).
|
For more details on how to import and use these, see the [Eggs README](./eggs/README.md).
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
This repository also tracks global configuration files that maintain system state and environment consistency:
|
||||||
|
- `.bash_aliases`: Tracks persistent command-line aliases for custom environment workflows.
|
||||||
|
|
||||||
## Future Scope
|
## Future Scope
|
||||||
|
|
||||||
This repository is intended to expand into a comprehensive hub for all configuration types beyond Docker and Pterodactyl, including:
|
This repository is intended to expand into a comprehensive hub for all configuration types beyond Docker and Pterodactyl, including:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
## HARD MANDATE: PLAN BEFORE ACTION
|
## HARD MANDATE: PLAN BEFORE ACTION
|
||||||
- **No Unapproved State Changes:** You MUST NOT execute any tool that modifies the filesystem, runs shell commands (excluding read-only discovery like `ls` or `grep`), or changes system state without first presenting a detailed plan and receiving an explicit "Directive" (e.g., "proceed") from the user. This rule is absolute. REQUIRED!
|
- **No Unapproved State Changes:** You MUST NOT execute any tool that modifies the filesystem, runs shell commands (excluding read-only discovery like `ls` or `grep`), or changes system state without first presenting a detailed plan and receiving an explicit "Directive" (e.g., "proceed") from the user. This rule is absolute. REQUIRED!
|
||||||
|
|
||||||
## Gemini Added Memories
|
## Added Memories
|
||||||
- When working on the Hugo wiki, docs, or Caddy routing, immediately run the docs_architecture skill to understand the decoupled architecture, deployment logic, and known Docker/Caddy networking quirks.
|
- When working on the Hugo wiki, docs, or Caddy routing, immediately run the docs_architecture skill to understand the decoupled architecture, deployment logic, and known Docker/Caddy networking quirks.
|
||||||
- When writing Hugo/Blowfish Markdown front matter, always include 'author', 'date' (ISO-8601), 'lastmod' (YYYY-MM-DD), and 'tags' (list) at the bottom of the metadata block.
|
- When writing Hugo/Blowfish Markdown front matter, always include 'author', 'date' (ISO-8601), 'lastmod' (YYYY-MM-DD), and 'tags' (list) at the bottom of the metadata block.
|
||||||
- Never perform manual builds or deployments of the Hugo sites to bypass the CI/CD pipeline. Always rely on the Gitea Actions workflows for deployment.
|
- Never perform manual builds or deployments of the Hugo sites to bypass the CI/CD pipeline. Always rely on the Gitea Actions workflows for deployment.
|
||||||
|
|||||||
BIN
ai/skills/.DS_Store
vendored
BIN
ai/skills/.DS_Store
vendored
Binary file not shown.
@@ -1,33 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: docs_architecture
|
||||||
|
description: Exact technical architecture, routing, and deployment workflow for WompMacho's decoupled Hugo/Gitea/Nginx Wiki infrastructure.
|
||||||
|
---
|
||||||
|
|
||||||
# Docs Architecture Skill
|
# Docs Architecture Skill
|
||||||
|
|
||||||
This skill documents the exact technical architecture, routing, and deployment workflow for WompMacho's decoupled Hugo/Gitea/Caddy Wiki infrastructure.
|
This skill documents the exact technical architecture, routing, and deployment workflow for WompMacho's decoupled Hugo/Gitea/Nginx Wiki infrastructure.
|
||||||
|
|
||||||
## 🏗️ 1. Core Architecture (Decoupled Repositories)
|
## 🏗️ 1. Core Architecture (Decoupled Repositories)
|
||||||
|
|
||||||
The system is split into three independent Git repositories hosted on `git.wompmacho.com`:
|
The system is split into three independent Git repositories hosted on `git.wompmacho.com`:
|
||||||
|
|
||||||
1. **Framework (`/srv/dev/hugo/wiki`)**: Repository `hugo-framework`
|
1. **Framework (`/srv/dev/hugo/wiki`)**: Repository `hugo-framework`
|
||||||
* **Purpose**: The central Hugo engine. Contains the Blowfish theme as a submodule (`themes/blowfish`) and the master `config/_default/params.toml`.
|
* **Purpose**: The central Hugo engine. The Blowfish theme is fully **decoupled/vendored** directly into the project's root folders (`layouts/`, `assets/`, `static/`, `data/`, `i18n/`, `archetypes/`). No theme submodules are used, protecting the site from upstream updates breaking the build.
|
||||||
* **Rule**: NEVER place content (`.md` files) here. NEVER place environment-specific overrides (like hardcoded `editURLs` or top-level menus) in the global `params.toml`.
|
* **Rule**: NEVER place content (`.md` files) here. NEVER place environment-specific overrides (like hardcoded `editURLs` or top-level menus) in the global config.
|
||||||
|
|
||||||
2. **Public Wiki (`/srv/docs/public`)**: Repository `docs-public`
|
2. **Public Wiki (`/srv/docs/public`)**: Repository `docs-public`
|
||||||
* **Purpose**: Public-facing markdown notes, assets (`/static`), and its own top-bar menu configuration (`config/_default/menus.en.toml`).
|
* **Purpose**: Public-facing markdown notes, assets (`/static`), and its own top-bar menu configuration (`config/_default/menus.en.toml`).
|
||||||
* **Deployment Target**: `/srv/caddy/sites/wiki`
|
* **Deployment Target**: `/srv/www/docs-public`
|
||||||
* **Live URL**: `https://wiki.wompmacho.com` (proxied by NPM).
|
* **Live URL**: `https://wiki.wompmacho.com` (proxied by NPM).
|
||||||
|
|
||||||
3. **Private Wiki (`/srv/docs/private`)**: Repository `docs-private`
|
3. **Private Wiki (`/srv/docs/private`)**: Repository `docs-private`
|
||||||
* **Purpose**: Personal, private markdown notes, assets (`/static`), and its own top-bar menu configuration (`config/_default/menus.en.toml`).
|
* **Purpose**: Personal, private markdown notes, assets (`/static`), and its own top-bar menu configuration (`config/_default/menus.en.toml`).
|
||||||
* **Deployment Target**: `/srv/caddy/sites/private-wiki`
|
* **Deployment Target**: `/srv/www/docs-private`
|
||||||
* **Live URL**: `http://10.0.0.190:9897` (or `http://private` via NPM).
|
* **Live URL**: `http://10.0.0.190:9897` (or `http://private` via NPM).
|
||||||
|
|
||||||
## 💻 2. Local Development ("Live Link")
|
## 💻 2. Local Development ("Live Link" Automation)
|
||||||
|
|
||||||
When working on content, the user uses **Code-Server**. To see real-time updates without deploying, the `hugo-framework` repository uses symbolic links.
|
When working on content, the user uses **Code-Server**. To see real-time updates without deploying, the framework repository uses automated symlinking and preview scripts.
|
||||||
|
|
||||||
* `content -> /srv/docs/public`
|
* **`preview.sh [public|private]`**:
|
||||||
* `static -> /srv/docs/public/static`
|
* Creates symlinks: `content -> /srv/docs/[public|private]` and `static -> /srv/docs/[public|private]/static`.
|
||||||
|
* Overwrites or symlinks specific config overrides (like `menus.en.toml`).
|
||||||
**Command to start local preview**: `cd /srv/dev/hugo/wiki && hugo server --bind 0.0.0.0 --appendPort=false --baseURL="/"`
|
* Executes `run-dev.sh` to start the local preview server.
|
||||||
|
* Traps exits (Ctrl+C, normal exit) to cleanly restore standard framework defaults and remove preview symlinks automatically.
|
||||||
|
* **`run-dev.sh`**:
|
||||||
|
* Launches the server bound to `0.0.0.0:1313` with code-server proxy parameters.
|
||||||
|
* Passes `--renderToMemory` to prevent local watch loops and file write latency.
|
||||||
|
|
||||||
## 🚀 3. CI/CD Pipeline (Gitea Actions)
|
## 🚀 3. CI/CD Pipeline (Gitea Actions)
|
||||||
|
|
||||||
@@ -35,7 +44,7 @@ Both `docs-public` and `docs-private` have a `.gitea/workflows/deploy.yaml` acti
|
|||||||
|
|
||||||
### The Build Process
|
### The Build Process
|
||||||
1. **Checkout Content**: The runner checks out the markdown content.
|
1. **Checkout Content**: The runner checks out the markdown content.
|
||||||
2. **Clone Framework**: The runner natively clones the `hugo-framework` (including submodules) using `git clone --recurse-submodules https://git.wompmacho.com/wompmacho/hugo-framework.git`.
|
2. **Clone Framework**: The runner clones the central `hugo-framework` repository directly. Because the Blowfish theme is fully decoupled, there is no need for submodule initialization.
|
||||||
3. **Inject Content**: Markdown and static files are copied into the framework's workspace.
|
3. **Inject Content**: Markdown and static files are copied into the framework's workspace.
|
||||||
4. **Inject Dynamic Menus**: The `config/_default/menus.en.toml` is injected, automatically overriding the framework's top navigation bar for that specific site.
|
4. **Inject Dynamic Menus**: The `config/_default/menus.en.toml` is injected, automatically overriding the framework's top navigation bar for that specific site.
|
||||||
5. **Inject Edit Links (CRITICAL RULE)**: The "Edit this page" link on the bottom of posts MUST NOT be configured via a `params.toml` override, as Hugo replaces the entire file instead of merging it, destroying the Blowfish styling.
|
5. **Inject Edit Links (CRITICAL RULE)**: The "Edit this page" link on the bottom of posts MUST NOT be configured via a `params.toml` override, as Hugo replaces the entire file instead of merging it, destroying the Blowfish styling.
|
||||||
@@ -45,17 +54,18 @@ Both `docs-public` and `docs-private` have a `.gitea/workflows/deploy.yaml` acti
|
|||||||
|
|
||||||
## 🌐 4. Web Server Routing (Nginx)
|
## 🌐 4. Web Server Routing (Nginx)
|
||||||
|
|
||||||
The system relies on two extremely lightweight `nginx:alpine` containers defined in `gitea/docker-compose.yaml` to serve static files. They act as "dumb" file servers behind Nginx Proxy Manager (NPM), bypassing the complex Host header validation and 0-byte security issues inherent to Caddy.
|
The system relies on two extremely lightweight `nginx:alpine` containers defined in `gitea/docker-compose.yaml` to serve static files. They act as file servers behind Nginx Proxy Manager (NPM).
|
||||||
|
|
||||||
1. **Public (`public_wiki` - Port 9896)**:
|
1. **Public (`public_wiki` - Port 9896)**:
|
||||||
* Mounts `/srv/caddy/sites/wiki` as read-only.
|
* Mounts `/srv/www/docs-public` as read-only.
|
||||||
* NPM routes `wiki.wompmacho.com` to Docker Host IP on port `9896`.
|
* NPM routes `wiki.wompmacho.com` to Docker Host IP on port `9896`.
|
||||||
2. **Private (`private_wiki` - Port 9897)**:
|
2. **Private (`private_wiki` - Port 9897)**:
|
||||||
* Mounts `/srv/caddy/sites/private-wiki` as read-only.
|
* Mounts `/srv/www/docs-private` as read-only.
|
||||||
* NPM routes `http://private` to Docker Host IP on port `9897`.
|
* NPM routes `http://private` to Docker Host IP on port `9897`.
|
||||||
|
|
||||||
## 🛠️ 5. Known Quirks and Formatting Rules
|
## 🛠️ 5. Known Quirks and Formatting Rules
|
||||||
|
|
||||||
* **Submodules**: Always use `git submodule add -b main <url>` and ensure the commit pointer is tracked. Do not copy theme files physically.
|
* **Decoupled Theme**: The Blowfish theme is committed directly in the main codebase. Do not use Git submodules to track the theme.
|
||||||
* **Blowfish Homepage**: If the homepage is blank, it means there are no recent posts. By default, Blowfish's `home` layout only lists files with `type: "article"` or `type: "posts"`. For generic folders, set `type: "page"` and `layout: "list"` in `_index.md`.
|
* **Blowfish Homepage**: If the homepage is blank, it means there are no recent posts. By default, Blowfish's `home` layout only lists files with `type: "article"` or `type: "posts"`. For generic folders, set `type: "page"` and `layout: "list"` in `_index.md`.
|
||||||
* **Frontmatter**: Blowfish expects standard Hugo frontmatter (`date:` as a string, `authors: ["Name"]` as an array). MkDocs-style nested YAML is incompatible and will crash the build.
|
* **Frontmatter**: Blowfish expects standard Hugo frontmatter (`date:` as a string, `authors: ["Name"]` as an array). MkDocs-style nested YAML is incompatible and will crash the build.
|
||||||
|
* **Troubleshooting**: See `references/troubleshooting.md` for handling theme updates, build loops, and TOC configuration.
|
||||||
24
ai/skills/docs_architecture/references/troubleshooting.md
Normal file
24
ai/skills/docs_architecture/references/troubleshooting.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Hugo/Blowfish Troubleshooting & Pitfalls
|
||||||
|
|
||||||
|
## 1. Infinite Rebuild Loops
|
||||||
|
If Hugo triggers a rebuild loop:
|
||||||
|
* **Cause**: Hugo watches the `resources/` directory, and the generation of cached/busted assets (like `admonitions.scss_...json`) triggers a new rebuild event.
|
||||||
|
* **Fix**: Add the following to `config/_default/hugo.toml`:
|
||||||
|
```toml
|
||||||
|
[watch]
|
||||||
|
ignore = ["resources/**"]
|
||||||
|
```
|
||||||
|
* **Tool Usage**: If the loop persists, start the server with polling to reduce sensitivity:
|
||||||
|
`hugo server --poll 1s`
|
||||||
|
|
||||||
|
## 2. Table of Contents (TOC) Issues
|
||||||
|
Blowfish manages TOC natively via `params.toml`. Avoid using manual `[TOC]` or `{{< toc >}}` in your Markdown unless explicitly required by a specific layout.
|
||||||
|
* **Standard Setup**: Ensure `showTableOfContents = true` is set in the `[article]` and `[list]` sections of your `config/_default/params.toml`.
|
||||||
|
* **Custom Layouts**: If TOC is rendering in the wrong place, it is likely due to an overridden `layouts/_default/single.html`. Revert to the native theme structure (`container` and `prose` classes) so the Blowfish partials can handle the placement.
|
||||||
|
* **TOC ScrollSpy Highlighting**: If the TOC fails to highlight active headings when scrolling, verify that `smartTOC = true` is enabled in `params.toml`. If it is nested inside the `[article]` section, ensure that the `layouts/partials/toc.html` checks:
|
||||||
|
`{{ if or .Site.Params.smartTOC .Site.Params.article.smartTOC }}`
|
||||||
|
to correctly resolve the nested parameter.
|
||||||
|
|
||||||
|
## 3. Theme Updates & Customization
|
||||||
|
* **Version mismatch**: When Hugo updates, theme templates often break (e.g., `can't evaluate field Locale`).
|
||||||
|
* **Decoupled Theme Installation**: The theme is fully decoupled/vendored in the root folders. If you need to update Blowfish, you will copy/vendor the new templates into `layouts/` and assets into `assets/`, but you must review custom changes to templates such as `layouts/_default/single.html` and `layouts/partials/toc.html` to make sure custom styles and scripts (like multiple TOC elements or active highlight overrides) are preserved.
|
||||||
146
ai/skills/homelab-infrastructure-review/SKILL.md
Normal file
146
ai/skills/homelab-infrastructure-review/SKILL.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
---
|
||||||
|
name: homelab-infrastructure-review
|
||||||
|
description: >
|
||||||
|
Performs a 3-way infrastructure reconciliation audit: homelab_Infra.md
|
||||||
|
documentation (including Nginx Proxy Route mappings) vs. live Docker container
|
||||||
|
states vs. homelable MCP canvas topology. Also audits the Homepage Dashboard
|
||||||
|
services configuration to ensure newly proxied services are accessible.
|
||||||
|
Triggers ONLY on explicit user command. No automation or cron.
|
||||||
|
version: 1.4.0
|
||||||
|
author: wompmacho
|
||||||
|
date: 2026-05-30
|
||||||
|
platforms: [linux, macos, windows]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [homelab, infrastructure, audit, reconciliation, mcp, docker, nginx, homepage]
|
||||||
|
related_skills: [docker-workflows, workflow-management]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Homelab Infrastructure Review
|
||||||
|
|
||||||
|
This skill has been refactored into an intelligent, context-aware auditing engine. It uses a YAML configuration (`scripts/audit_config.yaml`) to understand architectural conventions (aliases, ignored containers) and employs a robust, incremental development workflow. **Always test changes in small, atomic steps** (`syntax check -> run`) to avoid complex debugging cycles. The core logic now uses a Python helper (`scripts/load_config.py`) for configuration and `docker inspect` for accurate health checks.
|
||||||
|
|
||||||
|
## 1. Core Objectives
|
||||||
|
|
||||||
|
This skill provides a highly-disciplined auditing workflow that:
|
||||||
|
1. **Identifies all live services** on the host.
|
||||||
|
2. **Identifies all active proxies and domains** for services by parsing Nginx Proxy Manager config files.
|
||||||
|
3. **Cross-references** them against the source of truth documentation (`homelab_Infra.md`).
|
||||||
|
4. **Cross-references** them against the `homelable-mcp` visual canvas topology.
|
||||||
|
5. **Audits the Homepage Dashboard services configuration** (`/srv/configs/docker_compose/homepage/config/services.yaml`) to ensure all proxied services are accessible.
|
||||||
|
6. **Generates exact proposed changes** to:
|
||||||
|
- The markdown documentation (`homelab_Infra.md`)
|
||||||
|
- The `homelable-mcp` visual canvas nodes and edges
|
||||||
|
- The Homepage Dashboard services config (`services.yaml`), with services correctly split into their relevant hosts/categories.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Trigger Conditions
|
||||||
|
|
||||||
|
This skill is invoked **only on explicit, manual user command** — never automatically or periodically.
|
||||||
|
Acceptable trigger phrases: "review homelab", "infrastructure audit", "reconcile homelab", "homelab review", "check infrastructure".
|
||||||
|
|
||||||
|
No cron, no automated polling, and no silent background execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Source Inventory and Boundary Map
|
||||||
|
|
||||||
|
| Source Component | Path / Port / Endpoint | Permission Boundaries |
|
||||||
|
|---|---|---|
|
||||||
|
| **Infra Document** | `/srv/docs/public/projects/homelab/homelab_Infra.md` | Read: ✅ Write: ❌ (requires explicit user confirmation, never run git commands without auth) |
|
||||||
|
| **Live Docker State** | Host session via `docker ps` | Read: ✅ Write: ❌ (no automated container lifecycle modifications allowed) |
|
||||||
|
| **Nginx Proxy Configs** | `/app/nginxproxy/data/nginx/proxy_host/*.conf` | Read: ✅ Write: ❌ (read-only proxy blocks mapping) |
|
||||||
|
| **Homelable Backend API** | `http://localhost:9445/api/v1` | Read: ✅ Write: ❌ (node/edge canvas edits require explicit confirmation) |
|
||||||
|
| **Homepage Config** | `/srv/configs/docker_compose/homepage/config/services.yaml` | Read: ✅ Write: ❌ (requires explicit user confirmation) |
|
||||||
|
| **Homepage Layout** | `/srv/configs/docker_compose/homepage/config/settings.yaml` | Read: ✅ Write: ❌ (requires explicit user confirmation) |
|
||||||
|
| **Workspace Dir** | `/srv/configs/ai/` | Read: ✅ Write: ✅ (agent state and proposed drafts) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Reconciliation Logic & Execution Steps
|
||||||
|
|
||||||
|
### Step 1: Pre-Flight Safety Checks
|
||||||
|
Run **in order** before beginning any audit cycle. If any check fails, **simply inform the user directly in the session** and abort (do NOT log failures to a local `errors.log` file):
|
||||||
|
- **Lock Check:** Verify no concurrent audit process holds `/srv/configs/ai/.audit.lock`.
|
||||||
|
- **Backend Port Check:** Verify port `9445` is actively listening.
|
||||||
|
- **Backend API Check:** Verify backend returns `HTTP 200` on `/api/v1/health` using the parsed `MCP_SERVICE_KEY` from `/srv/configs/docker_compose/homelabel/.env`.
|
||||||
|
- **Source Files Check:** Verify both `/srv/configs/.bash_aliases` and `homelab_Infra.md` exist.
|
||||||
|
|
||||||
|
### Step 2: Nginx Proxy Parsing
|
||||||
|
Parse every proxy configuration file `/app/nginxproxy/data/nginx/proxy_host/*.conf` for:
|
||||||
|
- `server_name` (Proxy Domain name / Route)
|
||||||
|
- `set $port` (Forwarded Container Port)
|
||||||
|
|
||||||
|
Keep an internal lookup dictionary of `Forwarded Port -> Proxy Domain` for comparison.
|
||||||
|
|
||||||
|
### Step 3: Homepage Dashboard Audit
|
||||||
|
Read `/srv/configs/docker_compose/homepage/config/services.yaml` and compare the list of active proxy routes against the dashboard items. Propose adding newly proxied services so the user can navigate to them.
|
||||||
|
|
||||||
|
Homepage services must be strictly split into three distinct categories:
|
||||||
|
1. **Physical Devices & Networking related** (e.g. Proxmox, TrueNAS, Router, Pi-Hole, Moonlight streaming)
|
||||||
|
2. **Docker Host Containers** (e.g. nginx-proxy-manager, portainer, immich, vaultwarden, paperless-ngx, slopsmith-web)
|
||||||
|
3. **TrueNAS Host Containers** (e.g. radarr, sonarr, readarr, bazarr, prowlarr, calibre)
|
||||||
|
|
||||||
|
### Step 4: Documentation Formatting Standardization
|
||||||
|
|
||||||
|
Both the **`## Docker services`** and the **`## Truenas Services`** markdown tables inside `homelab_Infra.md` must strictly maintain this exact 5-column layout:
|
||||||
|
|
||||||
|
`| Container Name | Mapped Ports | Access | Proxy Route / Domain | Description / Role |`
|
||||||
|
|
||||||
|
Always parse and print matching columns when proposing or applying documentation edits.
|
||||||
|
|
||||||
|
### Step 5: Proxmox LXC/VM Auditing
|
||||||
|
Proxmox host virtualization represents nodes (containers/VMs) that communicate on the LAN subnet `10.0.0.0/16`.
|
||||||
|
- Parse the Proxmox Node Table under `### Proxmox node (laptop-proxmox)` in `homelab_Infra.md`.
|
||||||
|
- Retrieve all network canvas nodes from `http://localhost:9445/api/v1/nodes` matching types `vm`, `lxc`, or direct hosts.
|
||||||
|
- Compare and verify that active virtualization nodes on your network (such as `invidious` on `10.0.0.217` or `pterodactyl` on `10.0.0.110`) exist inside the documentation table. Propose additions for any unregistered LXCs or VMs.
|
||||||
|
|
||||||
|
### Step 6: Homelable MCP Canvas Styling Standards
|
||||||
|
|
||||||
|
When registering new Docker containers on the visual canvas using the `POST` endpoint at `/api/v1/nodes`, you must strictly match the styling of other standard services (use `gitea_runner` as reference):
|
||||||
|
|
||||||
|
- `"type"` must be `"docker_container"`
|
||||||
|
- `"hostname"` must be `"docker"`
|
||||||
|
- `"ip"` must be `""` (empty string, as they reside behind parent container networking)
|
||||||
|
- `"width"` must be `140.0`
|
||||||
|
- `"height"` must be `50.0`
|
||||||
|
- `"bottom_handles"` must be `1`
|
||||||
|
- `"custom_colors"` must be `{"show_services": false}`
|
||||||
|
- `"parent_id"` must be set to the Docker parent host node UUID (e.g. `ee61411a-018a-4471-8a76-bcd4a1f5579a`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Safety and Guardrails
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 1. **No silent writes:** All modifications to `homelab_Infra.md`, the `homelable-mcp` canvas, or `services.yaml` must be presented as a clear diff block and receive explicit, manual user validation.
|
||||||
|
> 2. **NO UNAUTHORIZED GIT COMMANDS:** Never execute any Git commands (such as `git add`, `git commit`, `git status`, `git diff`, etc.) without the user's explicit, per-action authorization.
|
||||||
|
> 3. **Preserve Comments:** Do not remove, alter, or strip existing documentation comments or section text blocks.
|
||||||
|
> 4. **No Errors Log File:** Do not append failures to a local `errors.log` file. Output error details directly to the active console stream to keep workspace logs clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Technical Pitfalls & Workarounds
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> - **Windows CRLF line endings (`\r\n`):** The Homepage dashboard's `services.yaml` contains Windows CRLF terminators. Traditional bash/unix line-ending regex matches (such as `$`) will **silently fail** to match titles or categories. Always run `tr -d '\r'` to sanitize lines before attempting pattern matches.
|
||||||
|
> - **Direct REST API bypass:** Querying `homelable-mcp` on `8001` via SSE requires an async JSON-RPC transport negotiation which is fragile and slow. Instead, bypass the MCP layer entirely and query the `homelable-backend` REST API directly on port `9445` at `/api/v1/nodes`. Pass the header `"X-MCP-Service-Key"` parsed from `/srv/configs/docker_compose/homelabel/.env` for instant, authenticated, and robust network nodes lookup.
|
||||||
|
- **Active Port Verification (Stale Config Filter):** Nginx Proxy Manager configuration files on disk can be stale/leftover (e.g. Moonlight). To completely prevent reporting decommissioned configs, always verify that the port associated with any Nginx config is **actively listening on the host** (or mapped in active docker container port-mappings) before reporting it as an active proxy. If the port is inactive, ignore the proxy block completely.
|
||||||
|
- **TrueNAS SCALE Node Socket Verification:** Rather than setting up fragile credentials or SSH logins, TrueNAS SCALE container port verification is achieved securely and in milliseconds using lightweight, non-destructive `/dev/tcp/10.0.0.2/[port]` network socket connection handshakes.
|
||||||
|
- **Proxmox VE API Live Verification:** To verify running LXCs and VMs, query `https://10.0.0.142:8006/api2/json/cluster/resources?type=vm` directly. Load `HOMEPAGE_VAR_proxmox_password` dynamically from `/srv/configs/docker_compose/homepage/.env` at runtime and strip outer double quotes before passing to the Authorization headers. Never hardcode credentials in codebase or transcripts.
|
||||||
|
- **Homepage Settings Layout Syncing:** When renaming category names in `/srv/configs/docker_compose/homepage/config/services.yaml` (such as `Arr` -> `Truenas Containers`, and `Containers` -> `Docker Containers`), the layout block names inside `/srv/configs/docker_compose/homepage/config/settings.yaml` under `layout:` **must be updated identically** or the cards will fail to render on the dashboard.
|
||||||
|
- **Do NOT Invent Public Proxies:** Do not invent public proxy domains or automatically propose changing local/VPN hrefs (such as `http://radarr/` or `http://calibre/`) to public domains (like `radarr.wompmacho.com`) in homepage configurations. They must remain as local/VPN URLs exactly as currently configured to preserve network isolation and security.
|
||||||
|
- **Multi-Alias Substring Matcher:** Nginx Proxy configs often specify multiple space-separated aliases (e.g. `server_name vaultwarden vaultwarden.wompmacho.com;`). When searching for these in `services.yaml` to detect missing cards, token-split the domains and verify if *any* alias matches as a substring within the homepage card `href` or name (case-insensitively). This prevents false-positives (such as flagging Vaultwarden or Gitea as missing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Skill File Structure
|
||||||
|
|
||||||
|
All scripts and files associated with this skill are stored in this self-contained directory:
|
||||||
|
- `SKILL.md` — Custom skill documentation and procedures.
|
||||||
|
- `scripts/homelab-infrastructure-review.sh` — Bash shell auditing script.
|
||||||
|
- `scripts/mcp_client.py` — Python client for direct database and API queries.
|
||||||
|
- `errors.log` — Legacy log file (decommissioned per safety rules — errors print straight to console).
|
||||||
|
- `.audit.lock` — Legitimate lock file preventing concurrent executions.
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Homelable Backend API Reference
|
||||||
|
|
||||||
|
Direct REST API access to Pouzor/homelable visualizer backend on port 9445. Bypasses the complex and slow Server-Sent Events (SSE) handshake required on port 8001.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
The backend API requires an unauthenticated sub-app service key. Read this key dynamically from:
|
||||||
|
`/srv/configs/docker_compose/homelabel/.env` -> `MCP_SERVICE_KEY`
|
||||||
|
|
||||||
|
Inject it into HTTP request headers:
|
||||||
|
`X-MCP-Service-Key: <MCP_SERVICE_KEY>`
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Health Check
|
||||||
|
`GET http://localhost:9445/api/v1/health`
|
||||||
|
Returns `{"status": "ok"}` on success (HTTP 200).
|
||||||
|
|
||||||
|
### 2. Retrieve Canvas Nodes
|
||||||
|
`GET http://localhost:9445/api/v1/nodes`
|
||||||
|
Returns a JSON array of active canvas nodes.
|
||||||
|
|
||||||
|
Example Node Object:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "computer",
|
||||||
|
"label": "Game Pc",
|
||||||
|
"hostname": "DESKTOP-BRHGMFT",
|
||||||
|
"ip": "10.0.0.109",
|
||||||
|
"status": "online",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"port": 11434,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"service_name": "Ollama"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "3fc16e75-9418-41b1-b4a2-d3859c9ce431"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Retrieve Canvas Edges
|
||||||
|
`GET http://localhost:9445/api/v1/edges`
|
||||||
|
Returns active connection edges between nodes.
|
||||||
|
|
||||||
|
### 4. Trigger Network Scan
|
||||||
|
`POST http://localhost:9445/api/v1/scan/trigger`
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Homelable Backend Direct REST API Reference
|
||||||
|
|
||||||
|
Instead of negotiating a complex and slow Server-Sent Events (SSE) stream over port `8001` (which requires async JSON-RPC sessions and frequently times out), query the REST API on `homelable-backend` directly.
|
||||||
|
|
||||||
|
## Endpoint & Port
|
||||||
|
- **Host Port:** `9445`
|
||||||
|
- **Base URL:** `http://localhost:9445/api/v1`
|
||||||
|
|
||||||
|
## Authentication Header
|
||||||
|
The backend is authenticated using a special service key header:
|
||||||
|
```
|
||||||
|
X-MCP-Service-Key: <MCP_SERVICE_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading Credentials Dynamically
|
||||||
|
To keep keys safe and secure (without leaking them to the LLM context window), read the key dynamically from the local `.env` configuration file:
|
||||||
|
- **Path:** `/srv/configs/docker_compose/homelabel/.env`
|
||||||
|
- **Variable Name:** `MCP_SERVICE_KEY`
|
||||||
|
|
||||||
|
## Common Endpoints
|
||||||
|
|
||||||
|
### 1. Health Check
|
||||||
|
- **Path:** `/api/v1/health`
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Response:** `{"status": "ok"}`
|
||||||
|
|
||||||
|
### 2. Retrieve Nodes (Canvas Devices)
|
||||||
|
- **Path:** `/api/v1/nodes`
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Response:** Array of node objects:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "3fc16e75-9418-41b1-b4a2-d3859c9ce431",
|
||||||
|
"type": "computer",
|
||||||
|
"label": "Game Pc",
|
||||||
|
"ip": "10.0.0.109",
|
||||||
|
"status": "online",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"port": 11434,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"service_name": "Ollama"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python Integration Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
env_path = "/srv/configs/docker_compose/homelabel/.env"
|
||||||
|
service_key = ""
|
||||||
|
|
||||||
|
if os.path.exists(env_path):
|
||||||
|
with open(env_path, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("MCP_SERVICE_KEY="):
|
||||||
|
service_key = line.split("=", 1)[1].strip()
|
||||||
|
|
||||||
|
headers = {"X-MCP-Service-Key": service_key}
|
||||||
|
resp = requests.get("http://localhost:9445/api/v1/nodes", headers=headers)
|
||||||
|
nodes = resp.json()
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Teach the skill how to map running containers to their documented names
|
||||||
|
# This resolves false positives for services running behind a single proxy container.
|
||||||
|
service_aliases:
|
||||||
|
nicotine: gluetun
|
||||||
|
torrent: gluetun
|
||||||
|
paperless-db: paperless-ngx
|
||||||
|
paperless-broker: paperless-ngx
|
||||||
|
guac-postgresql: guacamole
|
||||||
|
gitea-db-1: gitea
|
||||||
|
npm-sync: nginx-proxy-manager
|
||||||
|
homelable-mcp: homelable
|
||||||
|
homelable-backend: homelable
|
||||||
|
homelable-frontend: homelable
|
||||||
|
|
||||||
|
# Teach the skill how to map container names to their visual canvas labels
|
||||||
|
# This resolves false positives for services with user-friendly canvas names.
|
||||||
|
canvas_aliases:
|
||||||
|
docs-public: hugo (public)
|
||||||
|
docs-private: hugo (private)
|
||||||
|
|
||||||
|
# A simple list of containers to ALWAYS ignore during the audit.
|
||||||
|
# This is for transient, dev, or intentionally undocumented containers.
|
||||||
|
ignore_containers:
|
||||||
|
- openai
|
||||||
|
- slopsmith-web
|
||||||
|
- pihole-dns-shim # Managed by pihole LXC, not documented as a separate service.
|
||||||
|
- immich_machine_learning # Sub-service of Immich
|
||||||
|
- immich_postgres # Sub-service of Immich
|
||||||
|
- immich_redis # Sub-service of Immich
|
||||||
|
- guac-postgresql # Sub-service of Guacamole
|
||||||
|
- nginx-proxy-manager # Infrastructure, not a user-facing service
|
||||||
|
- npm-sync # Sub-service of Nginx Proxy Manager
|
||||||
|
- cloudflare-ddns # Infrastructure, not a user-facing service
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# homelab-infrastructure-review.sh
|
||||||
|
# Triggers a full, intelligent, context-aware reconciliation audit.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Configuration & Logging ──────────────────────────────────────────────────
|
||||||
|
AUDIT_CONFIG_FILE="/srv/configs/ai/skills/homelab-infrastructure-review/scripts/audit_config.yaml"
|
||||||
|
INFRA_FILE="/srv/docs/public/projects/homelab/homelab_Infra.md"
|
||||||
|
LOCK_FILE="/srv/configs/ai/.audit.lock"
|
||||||
|
log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
||||||
|
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
|
||||||
|
|
||||||
|
# ── Pre-flight & Lock ────────────────────────────────────────────────────────
|
||||||
|
acquire_lock() {
|
||||||
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
|
local pid; pid=$(cat "$LOCK_FILE" 2>/dev/null)
|
||||||
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log_error "Audit already running (PID $pid). Aborting."; exit 1
|
||||||
|
fi
|
||||||
|
log_info "Stale lock file found. Cleaning up."; rm -f "$LOCK_FILE"
|
||||||
|
fi
|
||||||
|
echo $$ > "$LOCK_FILE"; trap 'rm -f "$LOCK_FILE"' EXIT
|
||||||
|
}
|
||||||
|
preflight_checks() {
|
||||||
|
if [ ! -f "$AUDIT_CONFIG_FILE" ]; then log_error "audit_config.yaml not found. Aborting."; exit 1; fi
|
||||||
|
log_info "All pre-flight checks passed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Data Sources ─────────────────────────────────────────────────────────────
|
||||||
|
get_infra_docs() {
|
||||||
|
local in_services_section=false
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
if [[ "$line" =~ ^##[[:space:]]+(Docker|Truenas)[[:space:]]+services ]]; then
|
||||||
|
in_services_section=true
|
||||||
|
elif [[ "$line" =~ ^##[[:space:]] && ! "$line" =~ ^##[[:space:]]+(Docker|Truenas)[[:space:]]+services ]]; then
|
||||||
|
in_services_section=false
|
||||||
|
fi
|
||||||
|
if $in_services_section && [[ "$line" =~ ^\|[[:space:]]*\*\*([a-zA-Z0-9_-]+)\*\* ]]; then
|
||||||
|
IFS='|' read -r _ name _ <<< "$line"; printf '%s\n' "$(echo "$name" | tr -d '*' | xargs)"
|
||||||
|
fi
|
||||||
|
done < "$INFRA_FILE"
|
||||||
|
}
|
||||||
|
get_live_docker_containers() {
|
||||||
|
while IFS= read -r line; do
|
||||||
|
local name; name=$(echo "$line" | jq -r '.Names')
|
||||||
|
local health; health=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no_health_check{{end}}' "$name" 2>/dev/null || echo "error")
|
||||||
|
local status;
|
||||||
|
case "$health" in
|
||||||
|
healthy) status="healthy";;
|
||||||
|
unhealthy) status="UNHEALTHY";;
|
||||||
|
*) status="no_health_check";;
|
||||||
|
esac
|
||||||
|
printf '%s|%s\n' "$name" "$status"
|
||||||
|
done <<< "$(docker ps --format '{{json .}}')"
|
||||||
|
}
|
||||||
|
get_mcp_nodes() {
|
||||||
|
local service_key=""; if [ -f "/srv/configs/docker_compose/homelabel/.env" ]; then service_key=$(grep -E "^MCP_SERVICE_KEY=" "/srv/configs/docker_compose/homelabel/.env" | cut -d'=' -f2-); fi
|
||||||
|
curl -s -H "X-MCP-Service-Key: $service_key" "http://localhost:9445/api/v1/nodes" | jq -r '.[]? | .label // .name // "unknown"'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Reconciliation Engine ────────────────────────────────────────────────────
|
||||||
|
if [ -f "$AUDIT_CONFIG_FILE" ]; then
|
||||||
|
source <(python3 /srv/configs/ai/skills/homelab-infrastructure-review/scripts/load_config.py "$AUDIT_CONFIG_FILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
reconcile() {
|
||||||
|
local doc_services=(); while IFS= read -r name; do doc_services+=("$(echo "$name"|tr '[:upper:]' '[:lower:]')"); done < <(get_infra_docs)
|
||||||
|
local live_services=(); local untracked_report=""; local unhealthy_report=""
|
||||||
|
|
||||||
|
while IFS='|' read -r name status; do
|
||||||
|
local lower_name; lower_name=$(echo "$name" | tr '[:upper:]' '[:lower:]')
|
||||||
|
live_services+=("$lower_name")
|
||||||
|
if [[ " ${IGNORE_CONTAINERS} " == *" ${lower_name} "* ]]; then continue; fi
|
||||||
|
|
||||||
|
local found=false; for doc in "${doc_services[@]}"; do [ "$lower_name" = "$doc" ] && found=true && break; done
|
||||||
|
local alias_var="SERVICE_ALIAS_${lower_name//-/_}"
|
||||||
|
if ! $found && [[ -v $alias_var ]]; then
|
||||||
|
local target="${!alias_var}"; for doc in "${doc_services[@]}"; do [ "$target" = "$doc" ] && found=true && break; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $found; then untracked_report+=" ⚠ **$name** is running but not documented.\n"; fi
|
||||||
|
if [[ "$status" == "UNHEALTHY" ]]; then unhealthy_report+=" ✗ **$name** is **UNHEALTHY**.\n"; fi
|
||||||
|
done < <(get_live_docker_containers)
|
||||||
|
|
||||||
|
local missing_report=""
|
||||||
|
for doc in "${doc_services[@]}"; do
|
||||||
|
if [ "$doc" = "dev" ] || [ "$doc" = "calibre" ]; then continue; fi # Exceptions
|
||||||
|
local found=false; for live in "${live_services[@]}"; do [ "$doc" = "$live" ] && found=true && break; done
|
||||||
|
if ! $found; then missing_report+=" ✗ **$doc** is documented but not running.\n"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
local mcp_nodes=(); while IFS= read -r label; do mcp_nodes+=("$(echo "$label"|tr '[:upper:]' '[:lower:]'|tr ' _-' ' ')"); done < <(get_mcp_nodes)
|
||||||
|
local canvas_report=""
|
||||||
|
for name in "${live_services[@]}"; do
|
||||||
|
if [[ " ${IGNORE_CONTAINERS} " == *" ${name} "* ]]; then continue; fi
|
||||||
|
local found=false; for node in "${mcp_nodes[@]}"; do [[ "$node" == *"$name"* || "$name" == *"$node"* ]] && found=true && break; done
|
||||||
|
local alias_var="CANVAS_ALIAS_${name//-/_}"
|
||||||
|
if ! $found && [[ -v $alias_var ]]; then
|
||||||
|
local target="${!alias_var}"; for node in "${mcp_nodes[@]}"; do [ "$target" = "$node" ] && found=true && break; done
|
||||||
|
fi
|
||||||
|
if ! $found; then canvas_report+=" ⚠ **$name** is running but not on the canvas.\n"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf "## Homelab Audit Report\nGenerated: $(date)\n\n"
|
||||||
|
printf "### Docker vs. Documentation\n"
|
||||||
|
if [ -z "$untracked_report$missing_report$unhealthy_report" ]; then
|
||||||
|
printf " ✅ All services are aligned and healthy.\n"
|
||||||
|
else
|
||||||
|
printf "%s%s%s" "$untracked_report" "$missing_report" "$unhealthy_report"
|
||||||
|
fi
|
||||||
|
printf "\n### Docker vs. Canvas\n"
|
||||||
|
if [ -z "$canvas_report" ]; then printf " ✅ All services are mapped.\n"; else printf "%s" "$canvas_report"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main Execution ───────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
log_info "Starting Homelab Infrastructure Audit"
|
||||||
|
acquire_lock
|
||||||
|
preflight_checks
|
||||||
|
log_info "Running reconciliation..."
|
||||||
|
reconcile
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import yaml
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
if 'service_aliases' in config and config['service_aliases']:
|
||||||
|
for key, value in config['service_aliases'].items():
|
||||||
|
print(f"export SERVICE_ALIAS_{key.replace('-', '_')}='{value}'")
|
||||||
|
|
||||||
|
if 'canvas_aliases' in config and config['canvas_aliases']:
|
||||||
|
for key, value in config['canvas_aliases'].items():
|
||||||
|
print(f"export CANVAS_ALIAS_{key.replace('-', '_')}='{value}'")
|
||||||
|
|
||||||
|
if 'ignore_containers' in config and config['ignore_containers']:
|
||||||
|
ignore_list = " ".join(config['ignore_containers'])
|
||||||
|
print(f"export IGNORE_CONTAINERS='{ignore_list}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"# Config load failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
main(sys.argv[1])
|
||||||
|
else:
|
||||||
|
print("# Usage: python load_config.py <path_to_config.yaml>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
115
ai/skills/homelab-infrastructure-review/scripts/mcp_client.py
Normal file
115
ai/skills/homelab-infrastructure-review/scripts/mcp_client.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MCP Homelable Integration Module.
|
||||||
|
|
||||||
|
Communicates directly with the homelable-backend on port 9445
|
||||||
|
for canvas topology queries, node/edge retrieval, and scanning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BACKEND_URL = "http://localhost:9445/api/v1"
|
||||||
|
ENV_PATH = "/srv/configs/docker_compose/homelabel/.env"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_service_key() -> str:
|
||||||
|
"""Read service key dynamically from .env to authenticate request securely."""
|
||||||
|
try:
|
||||||
|
if os.path.exists(ENV_PATH):
|
||||||
|
with open(ENV_PATH, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("MCP_SERVICE_KEY="):
|
||||||
|
return line.split("=", 1)[1].strip()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Failed to read service key from {ENV_PATH}: {exc}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def health_check(endpoint: str = BACKEND_URL) -> bool:
|
||||||
|
"""Check if homelable-backend endpoint is reachable."""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
# Socket check fallback
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.settimeout(3)
|
||||||
|
s.connect(('localhost', 9445))
|
||||||
|
s.close()
|
||||||
|
logger.info("Backend socket check: port 9445 is open")
|
||||||
|
return True
|
||||||
|
except (ConnectionRefusedError, socket.timeout, OSError):
|
||||||
|
logger.warning("Backend socket check failed: port 9445 unreachable")
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = _get_service_key()
|
||||||
|
headers = {"X-MCP-Service-Key": key}
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{endpoint}/health", headers=headers, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
logger.info("Homelable-backend health check: OK")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Homelable-backend health check failed with HTTP {resp.status_code}")
|
||||||
|
return False
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
logger.warning(f"Homelable-backend health check exception: {exc}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_nodes(endpoint: str = BACKEND_URL) -> Optional[List[Dict]]:
|
||||||
|
"""Fetch nodes directly from homelable-backend."""
|
||||||
|
if not health_check(endpoint):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
logger.error("requests library not installed")
|
||||||
|
return None
|
||||||
|
|
||||||
|
key = _get_service_key()
|
||||||
|
headers = {"X-MCP-Service-Key": key}
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{endpoint}/nodes", headers=headers, timeout=5)
|
||||||
|
resp.raise_for_status()
|
||||||
|
nodes = resp.json()
|
||||||
|
logger.info(f"Fetched {len(nodes)} nodes directly from backend")
|
||||||
|
return nodes
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
logger.error(f"Failed to fetch nodes from backend: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def log_mcp_error(source: str, output: str) -> None:
|
||||||
|
"""Write MCP-related errors to errors.log."""
|
||||||
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
log_line = f"[{timestamp}] ERROR: Homelable {source} {output}.\n"
|
||||||
|
errors_log_path = "/srv/configs/ai/errors.log"
|
||||||
|
try:
|
||||||
|
with open(errors_log_path, "a") as f:
|
||||||
|
f.write(log_line)
|
||||||
|
except OSError:
|
||||||
|
print(log_line, file=__import__("sys").stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logger.info("Testing backend integration...")
|
||||||
|
if health_check():
|
||||||
|
nodes = fetch_nodes()
|
||||||
|
if nodes:
|
||||||
|
print(f"\nSuccessfully retrieved {len(nodes)} nodes:")
|
||||||
|
for node in nodes[:5]:
|
||||||
|
print(f" - {node.get('label', 'unknown')} ({node.get('ip', 'no IP')}) [Status: {node.get('status', 'n/a')}]")
|
||||||
|
else:
|
||||||
|
print("Failed to retrieve nodes.")
|
||||||
|
else:
|
||||||
|
print("Backend is unreachable.")
|
||||||
@@ -23,3 +23,12 @@ This skill allows Gemini to efficiently manage the lifecycle of project workflow
|
|||||||
- **Action Items**:
|
- **Action Items**:
|
||||||
- [ ] [Specific task]
|
- [ ] [Specific task]
|
||||||
- **Verification**: [Public-domain method to verify success]
|
- **Verification**: [Public-domain method to verify success]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitfalls & Best Practices
|
||||||
|
|
||||||
|
- **Avoid Absolute Positioning in Layouts**: When customizing themes, prefer Flexbox or Grid over `position: absolute` for structural elements like Sidebars or TOCs to prevent content overlap and preserve responsive layout integrity.
|
||||||
|
- **Global Theme Settings**: For TOC rendering in Blowfish, rely on `params.toml` configuration (`showTableOfContents = true`) rather than custom Markdown shortcodes (`[TOC]` or `{{< toc >}}`), which are often theme-incompatible.
|
||||||
|
- **Custom CSS vs Theme Overrides**: Always use `assets/css/custom.css` for structural overrides. Never modify theme-internal files (`themes/blowfish/...`), as they will be overwritten during updates.
|
||||||
|
- **Container Widths**: When overriding container widths, use relative units (percentages) and `!important` sparingly to avoid breaking theme-defined responsive breakpoints.
|
||||||
|
|||||||
18
docker_compose/farmOS/.gitignore
vendored
Normal file
18
docker_compose/farmOS/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# SECRETS AND CREDENTIALS (NEVER COMMIT THESE)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Environment variables (API keys, DB passwords, etc.)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Local Development DBs (often contain real-ish data)
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
dump.rdb
|
||||||
|
|
||||||
|
# Data
|
||||||
|
/keys
|
||||||
|
/sites
|
||||||
|
/db
|
||||||
40
docker_compose/farmOS/docker-compose.yaml
Normal file
40
docker_compose/farmOS/docker-compose.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# farmOS - https://farmos.org/ - https://git.drupalcode.org/project/farm/-/blob/4.x/docker/docker-compose.production.yml?ref_type=heads
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
farmOS-db:
|
||||||
|
container_name: farmOS-db
|
||||||
|
image: postgres:17
|
||||||
|
volumes:
|
||||||
|
- './db:/var/lib/postgresql/data'
|
||||||
|
environment:
|
||||||
|
# Set a strong password and optionally change the user/database name.
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
farmOS-www:
|
||||||
|
container_name: farmOS-www
|
||||||
|
depends_on:
|
||||||
|
- farmOS-db
|
||||||
|
# Update this to the latest stable version before deploying.
|
||||||
|
image: farmos/farmos:latest
|
||||||
|
volumes:
|
||||||
|
- './sites:/opt/drupal/web/sites'
|
||||||
|
- './keys:/opt/drupal/keys'
|
||||||
|
ports:
|
||||||
|
- '${FARMOS_HTTP_PORT}:80'
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
# Nginx Proxy Manager Automation
|
||||||
|
- "npm.proxy.domains=farm.wompmacho.com"
|
||||||
|
- "npm.proxy.host=${DOCKER_HOST_IP}"
|
||||||
|
- "npm.proxy.port=${FARMOS_HTTP_PORT}"
|
||||||
|
- "npm.proxy.scheme=http"
|
||||||
|
- "npm.proxy.ssl_verify=false"
|
||||||
|
- "npm.proxy.websockets=true"
|
||||||
|
- "npm.proxy.ssl.force=true"
|
||||||
|
- "npm.proxy.ssl.certificate.id=2"
|
||||||
|
#- "npm.proxy.advanced.config=location = / { return 301 /guacamole/; }"
|
||||||
|
# Pi-hole Automation (Point to Docker Host/Proxy IP)
|
||||||
|
- "pihole.custom-record=[[\"farm.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"
|
||||||
@@ -2,21 +2,37 @@
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: ghcr.io/pouzor/homelable-backend:latest
|
image: ghcr.io/pouzor/homelable-backend:2.5.1
|
||||||
container_name: homelable-backend
|
container_name: homelable-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- AUTH_USERNAME=${AUTH_USERNAME}
|
- AUTH_USERNAME=${AUTH_USERNAME}
|
||||||
- AUTH_PASSWORD_HASH=${AUTH_PASSWORD_HASH} # Use a bcrypt hash for security
|
- AUTH_PASSWORD_HASH=${AUTH_PASSWORD_HASH} # Use a bcrypt hash for security
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
|
- LIVEVIEW_KEY=${LIVEVIEW_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
ports:
|
ports:
|
||||||
- "${HOMELABEL_BACK_PORT}:8000"
|
- "${HOMELABEL_BACK_PORT}:8000"
|
||||||
|
|
||||||
|
mcp:
|
||||||
|
image: ghcr.io/pouzor/homelable-mcp:2.5.1
|
||||||
|
container_name: homelable-mcp
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
environment:
|
||||||
|
BACKEND_URL: "http://backend:8000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
#image: ghcr.io/pouzor/homelable-frontend-standalone:latest # standalone version
|
#image: ghcr.io/pouzor/homelable-frontend-standalone:latest # standalone version
|
||||||
image: ghcr.io/pouzor/homelable-frontend:latest
|
image: ghcr.io/pouzor/homelable-frontend:2.5.1
|
||||||
container_name: homelable-frontend
|
container_name: homelable-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -25,12 +41,12 @@
|
|||||||
- backend
|
- backend
|
||||||
labels:
|
labels:
|
||||||
# Nginx Proxy Manager Automation
|
# Nginx Proxy Manager Automation
|
||||||
- "npm.proxy.domains=homelable"
|
- "npm.proxy.domains=homelable.wompmacho.com"
|
||||||
- "npm.proxy.host=${DOCKER_HOST_IP}"
|
- "npm.proxy.host=${DOCKER_HOST_IP}"
|
||||||
- "npm.proxy.port=${HOMELABEL_FRONT_PORT}"
|
- "npm.proxy.port=${HOMELABEL_FRONT_PORT}"
|
||||||
- "npm.proxy.scheme=http"
|
- "npm.proxy.scheme=http"
|
||||||
#- "npm.proxy.websockets=true"
|
- "npm.proxy.websockets=true"
|
||||||
#- "npm.proxy.ssl_verify=false"
|
#- "npm.proxy.ssl_verify=false"
|
||||||
#- "npm.proxy.ssl.force=true"
|
- "npm.proxy.ssl.force=true"
|
||||||
#- "npm.proxy.ssl.certificate.id=1"
|
- "npm.proxy.ssl.certificate.id=2"
|
||||||
- "pihole.custom-record=[[\"homelable\", \"${DOCKER_HOST_IP}\"]]"
|
- "pihole.custom-record=[[\"homelable.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
type: pterodactyl
|
type: pterodactyl
|
||||||
url: https://games.wompmacho.com
|
url: https://games.wompmacho.com
|
||||||
key: {{HOMEPAGE_VAR_pterodactyl_apikey}}
|
key: {{HOMEPAGE_VAR_pterodactyl_apikey}}
|
||||||
- Arr:
|
- Truenas Containers:
|
||||||
- Jellyfin:
|
- Jellyfin:
|
||||||
icon: jellyfin.svg
|
icon: jellyfin.svg
|
||||||
href: "https://jellyfin.wompmacho.com/"
|
href: "https://jellyfin.wompmacho.com/"
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
type: prowlarr
|
type: prowlarr
|
||||||
url: http://truenas:30050
|
url: http://truenas:30050
|
||||||
key: {{HOMEPAGE_VAR_prowlarr_apikey}}
|
key: {{HOMEPAGE_VAR_prowlarr_apikey}}
|
||||||
- Containers:
|
- Docker Containers:
|
||||||
- vaultwarden:
|
- vaultwarden:
|
||||||
icon: vaultwarden.svg
|
icon: vaultwarden.svg
|
||||||
href: "https://vaultwarden.wompmacho.com"
|
href: "https://vaultwarden.wompmacho.com"
|
||||||
@@ -197,11 +197,6 @@
|
|||||||
href: "https://wompmacho.com/login"
|
href: "https://wompmacho.com/login"
|
||||||
target: _self
|
target: _self
|
||||||
description: LinkStack
|
description: LinkStack
|
||||||
# - sure:
|
|
||||||
# icon: http://sure/assets/logomark-color-92ed1e7e.svg
|
|
||||||
# href: "http://sure/"
|
|
||||||
# target: _self
|
|
||||||
# description: sure finance app
|
|
||||||
- paperless:
|
- paperless:
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/paperless-ngx.png
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/paperless-ngx.png
|
||||||
href: "http://paperless/"
|
href: "http://paperless/"
|
||||||
@@ -248,11 +243,11 @@
|
|||||||
href: "http://reaper/"
|
href: "http://reaper/"
|
||||||
target: _self
|
target: _self
|
||||||
description: reaper in docker
|
description: reaper in docker
|
||||||
- Slopsmith-Web:
|
# - Slopsmith-Web:
|
||||||
icon: https://cdn2.steamgriddb.com/icon/dfb61b74af460c2fd68bb8266f9f0814/32/256x256.png
|
# icon: https://cdn2.steamgriddb.com/icon/dfb61b74af460c2fd68bb8266f9f0814/32/256x256.png
|
||||||
href: "https://slopsmith/"
|
# href: "https://slopsmith/"
|
||||||
target: _self
|
# target: _self
|
||||||
description: slopsmith in docker
|
# description: slopsmith in docker
|
||||||
- guacamole:
|
- guacamole:
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/apache-guacamole.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/apache-guacamole.svg
|
||||||
href: "https://guac.wompmacho.com/"
|
href: "https://guac.wompmacho.com/"
|
||||||
@@ -268,3 +263,13 @@
|
|||||||
href: "https://neko.wompmacho.com/"
|
href: "https://neko.wompmacho.com/"
|
||||||
target: _self
|
target: _self
|
||||||
description: neko browser
|
description: neko browser
|
||||||
|
- homelable:
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homelable.svg
|
||||||
|
href: "https://homelable.wompmacho.com/view?key=live"
|
||||||
|
target: _self
|
||||||
|
description: homelable
|
||||||
|
- farmOS:
|
||||||
|
icon: https://cdn.fosstodon.org/accounts/avatars/109/321/909/625/316/786/original/763a2ed219103fd0.jpg
|
||||||
|
href: "https://farm.wompmacho.com/"
|
||||||
|
target: _self
|
||||||
|
description: farmOS
|
||||||
@@ -23,10 +23,10 @@ layout:
|
|||||||
useEqualHeights: true
|
useEqualHeights: true
|
||||||
style: row
|
style: row
|
||||||
columns: 4
|
columns: 4
|
||||||
Containers:
|
Docker Containers:
|
||||||
style: row
|
style: row
|
||||||
columns: 8
|
columns: 8
|
||||||
Arr:
|
Truenas Containers:
|
||||||
useEqualHeights: true
|
useEqualHeights: true
|
||||||
style: row
|
style: row
|
||||||
columns: 5
|
columns: 5
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Open WebUI - https://docs.openwebui.com/getting-started/quick-start/
|
# Open WebUI - https://docs.openwebui.com/getting-started/quick-start/
|
||||||
services:
|
services:
|
||||||
openwebui:
|
openai:
|
||||||
container_name: openai
|
container_name: openai
|
||||||
image: ghcr.io/open-webui/open-webui:latest
|
image: ghcr.io/open-webui/open-webui:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# paperless-ngx - https://awesome-docker-compose.com/paperless-ngx
|
# paperless-ngx - https://awesome-docker-compose.com/paperless-ngx
|
||||||
services:
|
services:
|
||||||
broker:
|
broker:
|
||||||
|
container_name: paperless-broker
|
||||||
image: docker.io/library/redis:7
|
image: docker.io/library/redis:7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ${PAPERLESS_DATA}/redis-data:/data
|
- ${PAPERLESS_DATA}/redis-data:/data
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
container_name: paperless-db
|
||||||
image: docker.io/library/postgres:16
|
image: docker.io/library/postgres:16
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
@@ -17,6 +19,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
|
container_name: paperless-ngx
|
||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
Reference in New Issue
Block a user