Compare commits

...

34 Commits

37 changed files with 1365 additions and 64 deletions

View File

@@ -8,11 +8,100 @@ grep -E "set .port|server_name" /app/nginxproxy/data/nginx/proxy_host/*.conf \
| sort -n \
| column -t -N "PORT,TARGET,OTHER_TARGET" -W "OTHER_TARGET" -s ' ' -o ' | ';
}
alias ports='_ports'
alias nginx='_ports'
# 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
alias src='source ~/.bash_aliases'
alias edit='vim /srv/configs/.bash_aliases'
# Docker Image Version Tag checker
function _tagger() {
if [ -z "$1" ]; then
echo "Usage: tag <image>:<tag>"
return 1
fi
# Split image and tag safely
local INPUT="$1"
local REPO="${INPUT%%:*}"
local TAG="${INPUT##*:}"
# If no tag provided, default to latest
[ "$REPO" = "$TAG" ] && TAG="latest"
# Handle official library images
local API_REPO="$REPO"
[[ "$REPO" != *"/"* ]] && API_REPO="library/$REPO"
# 1. Grab the exact unique digest for the target tag
local DIGEST
DIGEST=$(curl -s "https://hub.docker.com/v2/repositories/${API_REPO}/tags/${TAG}" | jq -r '.digest // empty')
if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then
echo "Error: Image or tag not found on Docker Hub."
return 1
fi
# 2. Get the sibling tag sharing the same digest, skipping text labels
local VERSION
VERSION=$(curl -s "https://hub.docker.com/v2/repositories/${API_REPO}/tags/?page_size=100" \
| jq -r --arg dig "$DIGEST" \
'.results[] | select(.digest == $dig) | .name' \
| grep -E -v 'latest|alpine|slim|debian|bullseye|bookworm|ubuntu|test|rc|nightly' \
| head -n 1)
# 3. Output results
if [ -n "$VERSION" ]; then
echo "${REPO}:${VERSION}"
else
echo "${REPO}:${TAG}"
fi
}
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
View File

@@ -40,3 +40,6 @@ dump.rdb
# Logs
*.log
# Desktop Services Store
.DS_Store

View File

@@ -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).
### 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
This repository is intended to expand into a comprehensive hub for all configuration types beyond Docker and Pterodactyl, including:

View File

@@ -1,7 +1,7 @@
## 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!
## 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 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.

BIN
ai/skills/.DS_Store vendored

Binary file not shown.

View File

@@ -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
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)
The system is split into three independent Git repositories hosted on `git.wompmacho.com`:
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`.
* **Rule**: NEVER place content (`.md` files) here. NEVER place environment-specific overrides (like hardcoded `editURLs` or top-level menus) in the global `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 config.
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`).
* **Deployment Target**: `/srv/caddy/sites/wiki`
* **Deployment Target**: `/srv/www/docs-public`
* **Live URL**: `https://wiki.wompmacho.com` (proxied by NPM).
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`).
* **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).
## 💻 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`
* `static -> /srv/docs/public/static`
**Command to start local preview**: `cd /srv/dev/hugo/wiki && hugo server --bind 0.0.0.0 --appendPort=false --baseURL="/"`
* **`preview.sh [public|private]`**:
* Creates symlinks: `content -> /srv/docs/[public|private]` and `static -> /srv/docs/[public|private]/static`.
* Overwrites or symlinks specific config overrides (like `menus.en.toml`).
* 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)
@@ -35,7 +44,7 @@ Both `docs-public` and `docs-private` have a `.gitea/workflows/deploy.yaml` acti
### The Build Process
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.
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.
@@ -45,17 +54,18 @@ Both `docs-public` and `docs-private` have a `.gitea/workflows/deploy.yaml` acti
## 🌐 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)**:
* 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`.
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`.
## 🛠️ 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`.
* **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.

View 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.

View File

@@ -0,0 +1,42 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

View File

@@ -0,0 +1,47 @@
# ADR Format
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
Create the `docs/adr/` directory lazily — only when the first ADR is needed.
## Template
```md
# {Short title of the decision}
{1-3 sentences: what's the context, what did we decide, and why.}
```
That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
## Optional sections
Only include these when they add genuine value. Most ADRs won't need them.
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
- **Considered Options** — only when the rejected alternatives are worth remembering
- **Consequences** — only when non-obvious downstream effects need to be called out
## Numbering
Scan `docs/adr/` for the highest existing number and increment by one.
## When to offer an ADR
All three of these must be true:
1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
### What qualifies
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.

View File

@@ -0,0 +1,63 @@
# CONTEXT.md Format
## Structure
```md
# {Context Name}
{One or two sentence description of what this context is and why it exists.}
## Language
**Order**:
{A one or two sentence description of the term}
_Avoid_: Purchase, transaction
**Invoice**:
A request for payment sent to a customer after delivery.
_Avoid_: Bill, payment request
**Customer**:
A person or organization that places orders.
_Avoid_: Client, buyer, account
```
## Rules
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.
- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution.
- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does.
- **Show relationships.** Use bold term names and express cardinality where obvious.
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts.
## Single vs multi-context repos
**Single context (most repos):** One `CONTEXT.md` at the repo root.
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
```md
# Context Map
## Contexts
- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
## Relationships
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
```
The skill infers which structure applies:
- If `CONTEXT-MAP.md` exists, read it to find contexts
- If only a root `CONTEXT.md` exists, single context
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.

View File

@@ -0,0 +1,88 @@
---
name: grill-with-docs
description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
---
<what-to-do>
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time, waiting for feedback on each question before continuing.
If a question can be answered by exploring the codebase, explore the codebase instead.
</what-to-do>
<supporting-info>
## Domain awareness
During codebase exploration, also look for existing documentation:
### File structure
Most repos have a single context:
```
/
├── CONTEXT.md
├── docs/
│ └── adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
```
/
├── CONTEXT-MAP.md
├── docs/
│ └── adr/ ← system-wide decisions
├── src/
│ ├── ordering/
│ │ ├── CONTEXT.md
│ │ └── docs/adr/ ← context-specific decisions
│ └── billing/
│ ├── CONTEXT.md
│ └── docs/adr/
```
Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
## During the session
### Challenge against the glossary
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
### Sharpen fuzzy language
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
### Discuss concrete scenarios
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
### Cross-reference with code
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
### Update CONTEXT.md inline
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
### Offer ADRs sparingly
Only offer to create an ADR when all three are true:
1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
</supporting-info>

View 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.

View File

@@ -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`

View File

@@ -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()
```

View File

@@ -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

View File

@@ -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 "$@"

View File

@@ -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)

View 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.")

View File

@@ -11,7 +11,9 @@ Strictly adhere to the rules and best practices defined in the local [STYLE_GUID
## Key Guidelines
- **Clear & Concise Writing**: Favor simple sentence structures and active voice.
- **Structural Integrity**: Use hierarchical headings correctly (H1, H2, H3).
- **Structural Integrity**: Use hierarchical headings correctly (H1, H2, H3). All headings must have an empty newline immediately following them in all sessions.
- **Semantic Markdown**: Use appropriate elements (lists, code blocks, tables) for clarity.
- **Google Style**: Follow the local style guide precisely for all formatting and wording.
- **Alerts & Callouts**: Use GitHub-style alerts (`> [!NOTE]`, `> [!IMPORTANT]`, etc.).
- **Spacing Enforcement**: Always guarantee a single empty newline immediately following every heading in all markdown (.md) documents.

View File

@@ -23,3 +23,12 @@ This skill allows Gemini to efficiently manage the lifecycle of project workflow
- **Action Items**:
- [ ] [Specific task]
- **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
View 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

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

View File

@@ -0,0 +1,80 @@
# apache-guacamole - https://github.com/code-loading/guacamole-docker-compose/blob/main/docker-compose.yaml
services:
# ---------------------------------------------------------
# Guacamole Daemon (guacd)
# ---------------------------------------------------------
guacd:
image: guacamole/guacd:1.6.0 # Guacd daemon handles RDP, SSH, VNC protocols
container_name: guacd
restart: unless-stopped
networks:
- guac_network
# ---------------------------------------------------------
# PostgreSQL Database Service
# ---------------------------------------------------------
guac-postgresql:
image: postgres:15
container_name: guac-postgresql
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRESQL_DATABASE} # Database name (from .env file)
POSTGRES_USER: ${POSTGRESQL_USERNAME} # Database username
POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD} # Database password
volumes:
# Mount startup script from guacamole container into postgres container to initialize DB schema
- "initdb:/docker-entrypoint-initdb.d:ro"
# (optional) Persist database data on host to survive container rebuilds:
# - "${HOME}/guacamole/postgres-data:/var/lib/postgresql/data"
networks:
- guac_network
# ---------------------------------------------------------
# Guacamole Web Application
# ---------------------------------------------------------
guacamole:
image: guacamole/guacamole:1.6.0 # Guacamole web app image
container_name: guacamole
restart: unless-stopped
environment:
POSTGRESQL_DATABASE: ${POSTGRESQL_DATABASE} # guacamole container also needs to be provided with DB name, user and password
POSTGRESQL_USERNAME: ${POSTGRESQL_USERNAME}
POSTGRESQL_PASSWORD: ${POSTGRESQL_PASSWORD}
POSTGRESQL_HOSTNAME: guac-postgresql # Must match service name above of postgres
POSTGRESQL_ENABLED: "true" # Enable PostgreSQL authentication backend
GUACD_HOSTNAME: guacd # Must match service name below of guacd
ports:
- ${GUAC_HTTP_PORT}:8080
volumes:
# Mount startup script from guacamole container into postgres container to initialize DB schema
- "initdb:/opt/guacamole/extensions/guacamole-auth-jdbc/postgresql/schema:ro"
depends_on:
- guacd
- guac-postgresql
networks:
- guac_network
labels:
# Nginx Proxy Manager Automation
- "npm.proxy.domains=guac.wompmacho.com"
- "npm.proxy.host=${DOCKER_HOST_IP}"
- "npm.proxy.port=${GUAC_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=[[\"guac.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"
# ---------------------------------------------------------
# Named Volume
# ---------------------------------------------------------
volumes:
initdb:
# ---------------------------------------------------------
# Shared Network
# ---------------------------------------------------------
networks:
guac_network:

View File

@@ -0,0 +1,17 @@
# Handbrake - https://github.com/jlesage/docker-handbrake
services:
handbrake:
image: jlesage/handbrake:latest
container_name: handbrake
ports:
- "5800:5800"
volumes:
- "/docker/appdata/handbrake:/config:rw"
- "/home/user/Videos:/storage:ro"
- "/home/user/HandBrake/watch:/watch:rw"
- "/home/user/HandBrake/output:/output:rw"
environment:
- USER_ID=1000
- GROUP_ID=1000
- TZ=America/New_York
restart: unless-stopped

5
docker_compose/homelabel/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# ------------------------------------------------------------------------------
# SECRETS AND CREDENTIALS (NEVER COMMIT THESE)
# ------------------------------------------------------------------------------
data/

View File

@@ -0,0 +1,52 @@
# homelable - https://github.com/Pouzor/homelable
---
services:
backend:
image: ghcr.io/pouzor/homelable-backend:2.5.1
container_name: homelable-backend
restart: unless-stopped
env_file:
- .env
environment:
- AUTH_USERNAME=${AUTH_USERNAME}
- AUTH_PASSWORD_HASH=${AUTH_PASSWORD_HASH} # Use a bcrypt hash for security
- SECRET_KEY=${SECRET_KEY}
- LIVEVIEW_KEY=${LIVEVIEW_KEY}
volumes:
- ./data:/app/data
ports:
- "${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:
#image: ghcr.io/pouzor/homelable-frontend-standalone:latest # standalone version
image: ghcr.io/pouzor/homelable-frontend:2.5.1
container_name: homelable-frontend
restart: unless-stopped
ports:
- "${HOMELABEL_FRONT_PORT}:80"
depends_on:
- backend
labels:
# Nginx Proxy Manager Automation
- "npm.proxy.domains=homelable.wompmacho.com"
- "npm.proxy.host=${DOCKER_HOST_IP}"
- "npm.proxy.port=${HOMELABEL_FRONT_PORT}"
- "npm.proxy.scheme=http"
- "npm.proxy.websockets=true"
#- "npm.proxy.ssl_verify=false"
- "npm.proxy.ssl.force=true"
- "npm.proxy.ssl.certificate.id=2"
- "pihole.custom-record=[[\"homelable.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"

View File

@@ -56,7 +56,7 @@
type: pterodactyl
url: https://games.wompmacho.com
key: {{HOMEPAGE_VAR_pterodactyl_apikey}}
- Arr:
- Truenas Containers:
- Jellyfin:
icon: jellyfin.svg
href: "https://jellyfin.wompmacho.com/"
@@ -151,7 +151,7 @@
type: prowlarr
url: http://truenas:30050
key: {{HOMEPAGE_VAR_prowlarr_apikey}}
- Containers:
- Docker Containers:
- vaultwarden:
icon: vaultwarden.svg
href: "https://vaultwarden.wompmacho.com"
@@ -197,21 +197,16 @@
href: "https://wompmacho.com/login"
target: _self
description: LinkStack
- sure:
icon: http://sure/assets/logomark-color-92ed1e7e.svg
href: "http://sure/"
target: _self
description: sure finance app
- paperless:
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/paperless-ngx.png
href: "http://paperless/"
target: _self
description: paperless-ngx
- open-webui:
icon: http://gemma/static/favicon.png
href: "http://gemma/"
- openai:
icon: http://openai/static/favicon.png
href: "http://openai/"
target: _self
description: open-webui
description: openai
- calibre:
icon: https://upload.wikimedia.org/wikipedia/commons/c/cf/Calibre_logo_3.png
href: "http://calibre/"
@@ -248,8 +243,33 @@
href: "http://reaper/"
target: _self
description: reaper in docker
- Slopsmith-Web:
icon: https://cdn2.steamgriddb.com/icon/dfb61b74af460c2fd68bb8266f9f0814/32/256x256.png
href: "https://slopsmith/"
# - Slopsmith-Web:
# icon: https://cdn2.steamgriddb.com/icon/dfb61b74af460c2fd68bb8266f9f0814/32/256x256.png
# href: "https://slopsmith/"
# target: _self
# description: slopsmith in docker
- guacamole:
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/apache-guacamole.svg
href: "https://guac.wompmacho.com/"
target: _self
description: slopsmith in docker
description: quac
- Invidious:
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/invidious.svg
href: "http://inv/"
target: _self
description: Invidious LXC
- neko:
icon: https://neko.wompmacho.com/img/logo.800bec71.svg
href: "https://neko.wompmacho.com/"
target: _self
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

View File

@@ -23,10 +23,10 @@ layout:
useEqualHeights: true
style: row
columns: 4
Containers:
Docker Containers:
style: row
columns: 8
Arr:
Truenas Containers:
useEqualHeights: true
style: row
columns: 5

View File

@@ -0,0 +1,33 @@
# moonlight-web-stream - https://github.com/MrCreativ3001/moonlight-web-stream
services:
# Session 1: Connecting to your current Ubuntu VM (apollo)
moonlight-apollo:
image: mrcreativ3001/moonlight-web-stream:latest
container_name: moonlight_gateway_apollo
restart: unless-stopped
network_mode: host # Bypasses Docker bridge networking
volumes:
# FIXED: Reverted to the original mount path. If the app auto-writes settings,
# it can put them here, but our environment variables will override the port.
- ./config/apollo:/app/config
environment:
# FIX: The binary explicitly looks for BIND_ADDRESS to overwrite the listener
- BIND_ADDRESS=0.0.0.0:${MOONLIGHT_PORT}
# FIX: Use the binary's exact env hook to set the WebRTC port ranges cleanly
- WEBRTC_PORT_RANGE=40000:40010
# FIX: Forces WebRTC to tell your browser exactly where to send the UDP packets
- WEBRTC_NAT_1TO1_HOST=${DOCKER_HOST_IP}
labels:
# Nginx Proxy Manager Automation
- "npm.proxy.domains=moonlight.wompmacho.com"
- "npm.proxy.host=${DOCKER_HOST_IP}"
- "npm.proxy.port=${MOONLIGHT_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"
# FIXED: Inject Custom Nginx Performance Directives via Labels
- "npm.advanced=proxy_http_version 1.1; proxy_set_header Upgrade $$http_upgrade; proxy_set_header Connection \"Upgrade\"; proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; proxy_buffering off; proxy_request_buffering off; chunked_transfer_encoding on; client_max_body_size 0;"
# Pi-hole Automation (Point to Docker Host/Proxy IP)
- "pihole.custom-record=[[\"moonlight.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"

View File

@@ -0,0 +1,33 @@
# neko - https://github.com/m1k1o/neko
# https://neko.m1k1o.net/docs/v3/configuration
services:
neko:
container_name: neko
#build:
# context: .
# dockerfile: dockerfile
#image: ghcr.io/m1k1o/neko/kde
image: ghcr.io/m1k1o/neko/nvidia-firefox
restart: "unless-stopped"
shm_size: "2gb"
ports:
- "${NECO_PORT}:8080"
- "${WEBRTC}:52000-52100/udp"
environment:
NEKO_DESKTOP_SCREEN: 1920x1080@30
NEKO_MEMBER_MULTIUSER_USER_PASSWORD: ${NEKO_MEMBER_MULTIUSER_USER_PASSWORD}
NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD: ${NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD}
NEKO_WEBRTC_EPR: 52000-52100
NEKO_WEBRTC_ICELITE: 1
# See: https://neko.m1k1o.net/docs/v3/configuration/webrtc#ip
# NEKO_NAT1TO1: <IP_ADDRESS>
labels:
# Nginx Proxy Manager Automation
- "npm.proxy.domains=neko.wompmacho.com"
- "npm.proxy.host=${DOCKER_HOST_IP}"
- "npm.proxy.port=${NECO_PORT}"
- "npm.proxy.scheme=http"
- "npm.proxy.websockets=true"
- "npm.proxy.ssl.force=true"
- "npm.proxy.ssl.certificate.id=2"
- "pihole.custom-record=[[\"moonlight.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"

View File

@@ -0,0 +1,14 @@
FROM ghcr.io/m1k1o/neko/kde:latest
# Switch to root to install packages
USER root
# Update packages and install your chosen browser
# Example for Firefox:
RUN apt-get update && apt-get install -y firefox-esr && apt-get clean
# Alternatively, for Chromium:
# RUN apt-get update && apt-get install -y chromium && apt-get clean
# Switch back to the 'neko' user
USER neko

View File

@@ -1,12 +0,0 @@
# Open WebUI - https://docs.openwebui.com/getting-started/quick-start/
services:
openwebui:
image: ghcr.io/open-webui/open-webui:main
restart: unless-stopped
ports:
- "${OPEN_WEB_UI_PORT}:8080"
volumes:
- ${OPEN_WEB_UI_DATA}:/app/backend/data
environment:
- OLLAMA_BASE_URL=${OPEN_WEB_UI_COMPUTE}
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}

5
docker_compose/openai/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# ------------------------------------------------------------------------------
# SECRETS AND CREDENTIALS (NEVER COMMIT THESE)
# ------------------------------------------------------------------------------
data/

View File

@@ -0,0 +1,24 @@
# Open WebUI - https://docs.openwebui.com/getting-started/quick-start/
services:
openai:
container_name: openai
image: ghcr.io/open-webui/open-webui:latest
restart: unless-stopped
ports:
- "${OPEN_WEB_UI_PORT}:8080"
volumes:
- ./data:/app/backend/data
environment:
- OLLAMA_BASE_URL=${OPEN_WEB_UI_COMPUTE}
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
labels:
# Nginx Proxy Manager Automation
- "npm.proxy.domains=openai"
- "npm.proxy.host=${DOCKER_HOST_IP}"
- "npm.proxy.port=${OPEN_WEB_UI_PORT}"
- "npm.proxy.scheme=http"
#- "npm.proxy.websockets=true"
#- "npm.proxy.ssl_verify=false"
#- "npm.proxy.ssl.force=true"
#- "npm.proxy.ssl.certificate.id=1"
- "pihole.custom-record=[[\"openai\", \"${DOCKER_HOST_IP}\"]]"

View File

@@ -1,12 +1,14 @@
# paperless-ngx - https://awesome-docker-compose.com/paperless-ngx
services:
broker:
container_name: paperless-broker
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- ${PAPERLESS_DATA}/redis-data:/data
db:
container_name: paperless-db
image: docker.io/library/postgres:16
restart: unless-stopped
volumes:
@@ -17,6 +19,7 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
webserver:
container_name: paperless-ngx
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:

View File

@@ -2,18 +2,19 @@
---
services:
vaultwarden:
image: vaultwarden/server:1.35.2
image: vaultwarden/server:1.36.0
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "${VAULT_DOMAIN}"
ROCKET_PORT: ${ROCKET_PORT}
ROCKET_ENV: production
ADMIN_TOKEN: ${ADMIN_TOKEN}
volumes:
- vaultwarden-mount:/data/
ports:
- 'LOCAL_PORT:80'
- 'SSL_PORT:443'
- '${LOCAL_PORT}:80'
- '${SSL_PORT}:443'
volumes:
vaultwarden-mount:

View File

@@ -1,4 +1,4 @@
# webtop -- https://docs.linuxserver.io/images/docker-webtop/#lossless-mode
# webtop -- https://docs.linuxserver.io/images/docker-webtop/
---
services:
webtop:
@@ -9,19 +9,40 @@ services:
- PGID=1000
- TZ=America/New_York
- TITLE=Webtop #optional
- PIXELFLUX_WAYLAND=true
- DRINODE=/dev/dri/renderD128
- DRI_NODE=/dev/dri/renderD128
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=all
- VK_ICD_FILENAMES=/etc/vulkan/icd.d/nvidia_icd.json
#- DISABLE_ZINK=true
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [compute, video, graphics, utility]
# security_opt:
# - seccomp:unconfined
dns:
- ${PIHOLE_SERVER}
shm_size: "1gb" #optional
shm_size: "2gb" #optional
volumes:
- ${WEBTOP_DATA}config:/config
ports:
- 7978:3000
- 7979:3001
networks:
- frontend
- ${WEBTOP_HTTP_PORT}:3000
- ${WEBTOP_HTTPS_PORT}:3001
restart: unless-stopped
networks:
frontend:
external: true
labels:
# Nginx Proxy Manager Automation
- "npm.proxy.domains=webtop.wompmacho.com"
- "npm.proxy.host=${DOCKER_HOST_IP}"
- "npm.proxy.port=${WEBTOP_HTTPS_PORT}"
- "npm.proxy.scheme=https"
- "npm.proxy.websockets=true"
- "npm.proxy.ssl.force=true"
- "npm.proxy.ssl.certificate.id=2"
- "npm.proxy.accesslist.id=1"
# Pi-hole Automation (Point to Docker Host/Proxy IP)
- "pihole.custom-record=[[\"webtop.wompmacho.com\", \"${DOCKER_HOST_IP}\"]]"