diff --git a/.gitea/scripts/README-update-readme.md b/.gitea/scripts/README-update-readme.md
new file mode 100644
index 0000000..4ad30f1
--- /dev/null
+++ b/.gitea/scripts/README-update-readme.md
@@ -0,0 +1,147 @@
+# Auto-Update README Workflow
+
+## Overview
+
+This workflow automatically updates the `README.md` file with the current list of ArgoCD applications based on the directory structure in `k8s/`.
+
+## How it works
+
+1. **Trigger**: Workflow runs automatically when changes are pushed to `k8s/**` paths
+2. **Scan**: Python script scans `k8s/` directory structure and finds all applications
+3. **Generate**: Creates README.md with badges for all found applications
+4. **Create PR**: If changes detected, creates a Merge Request for manual review
+
+## Files
+
+- `.gitea/workflows/update-readme.yaml` - GitHub Actions workflow
+- `.gitea/scripts/generate-readme.py` - Python script for README generation
+
+## Directory Structure
+
+The script expects the following k8s directory structure:
+
+```
+k8s/
+├── core/ # Core infrastructure applications
+│ ├── argocd/
+│ ├── authentik/
+│ └── ...
+├── apps/ # User applications
+│ ├── gitea/
+│ ├── immich/
+│ └── ...
+└── games/ # Game servers
+ ├── minecraft/
+ └── ...
+```
+
+Each subdirectory name becomes an application name in the README.
+
+## Required Secrets
+
+The workflow requires the following secrets to be configured in Gitea:
+
+| Secret | Description | Example |
+|--------|-------------|---------|
+| `GT_URL` | Gitea instance URL | `https://gt.hexor.cy` |
+| `GT_TOKEN` | Gitea API token with repo write access | `glpat-xxxxx...` |
+| `GT_OWNER` | Repository owner (username or org) | `ab` |
+| `GT_REPO` | Repository name | `homelab` |
+
+### How to create a Gitea Token
+
+1. Go to Settings → Applications → Generate New Token
+2. Give it a name like "README Update Bot"
+3. Select scopes: `repo` (Full control of repositories)
+4. Generate and copy the token
+5. Add it as a secret in repository settings
+
+## Badge Format
+
+Badges are generated using a predictable pattern:
+
+```markdown
+[](https://ag.hexor.cy/applications/argocd/app-name)
+```
+
+This allows you to immediately see which applications are:
+- ✅ Healthy and synced (green badge)
+- ⚠️ Degraded or out of sync (yellow badge)
+- ❌ Unhealthy or failed (red badge)
+
+## Manual Trigger
+
+You can manually trigger the workflow from Gitea:
+
+1. Go to Actions tab
+2. Select "Auto-update README" workflow
+3. Click "Run workflow"
+4. Select branch and run
+
+## Example Output
+
+The generated README will look like:
+
+```markdown
+# homelab
+
+ArgoCD homelab project
+
+## ArgoCD Applications Status
+
+| Application | Status |
+| :--- | :---: |
+| **argocd** | [](https://ag.hexor.cy/applications/argocd/argocd) |
+...
+```
+
+## Reviewing Pull Requests
+
+When the workflow creates a PR:
+
+1. Check the Actions tab for the workflow run details
+2. Review the PR in the Pull Requests tab
+3. Verify the application list matches your k8s/ structure
+4. Merge when ready
+
+The PR will include:
+- Updated application list
+- Timestamp of generation
+- Automatic commit message
+
+## Troubleshooting
+
+### No PR created
+
+- Check if there are actually changes in README.md
+- Verify secrets are configured correctly
+- Check workflow logs in Actions tab
+
+### Wrong applications listed
+
+- Verify k8s/ directory structure
+- Ensure folder names match expected application names
+- Check for hidden directories (starting with `.`)
+
+### Badge not loading
+
+- Verify ArgoCD badge API is accessible at `https://ag.hexor.cy`
+- Check application name matches ArgoCD application name
+- Ensure application exists in ArgoCD
+
+## Maintenance
+
+### Update badge URL
+
+If you need to change the badge URL pattern, edit:
+- `.gitea/scripts/generate-readme.py` - function `generate_badge_line()`
+
+### Change workflow trigger
+
+To modify when the workflow runs, edit:
+- `.gitea/workflows/update-readme.yaml` - `on:` section
+
+### Add new categories
+
+To add new categories (besides core/apps/games), edit:
+- `.gitea/scripts/generate-readme.py` - function `scan_k8s_directory()` and `generate_readme_content()`
diff --git a/.gitea/scripts/generate-readme.py b/.gitea/scripts/generate-readme.py
new file mode 100644
index 0000000..fac416d
--- /dev/null
+++ b/.gitea/scripts/generate-readme.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+"""
+Generate README.md with ArgoCD application status badges.
+Scans k8s/ directory structure to find all applications and generates badges for them.
+"""
+
+import os
+import sys
+from pathlib import Path
+from typing import Dict, List
+
+
+def scan_k8s_directory(k8s_path: str) -> Dict[str, List[str]]:
+ """
+ Scan k8s/ directory and return applications grouped by category.
+
+ Args:
+ k8s_path: Path to k8s directory
+
+ Returns:
+ Dictionary with categories as keys and lists of app names as values
+ """
+ apps_by_category = {
+ 'core': [],
+ 'apps': [],
+ 'games': []
+ }
+
+ k8s_dir = Path(k8s_path)
+
+ for category in apps_by_category.keys():
+ category_path = k8s_dir / category
+ if category_path.exists() and category_path.is_dir():
+ # Get all subdirectories (each subdirectory is an app)
+ apps = [
+ d.name for d in category_path.iterdir()
+ if d.is_dir() and not d.name.startswith('.')
+ ]
+ apps_by_category[category] = sorted(apps)
+
+ return apps_by_category
+
+
+def generate_badge_line(app_name: str) -> str:
+ """
+ Generate markdown line with badge for an application.
+
+ Args:
+ app_name: Name of the application
+
+ Returns:
+ Markdown formatted string with badge
+ """
+ badge_url = f"https://ag.hexor.cy/api/badge?name={app_name}&revision=true"
+ app_url = f"https://ag.hexor.cy/applications/argocd/{app_name}"
+ return f"| **{app_name}** | []({app_url}) |"
+
+
+def generate_readme_content(apps_by_category: Dict[str, List[str]]) -> str:
+ """
+ Generate README.md content with all applications.
+
+ Args:
+ apps_by_category: Dictionary with apps grouped by category
+
+ Returns:
+ Complete README.md content
+ """
+ lines = [
+ "# homelab",
+ "",
+ "ArgoCD homelab project",
+ "",
+ "## ArgoCD Applications Status",
+ "",
+ "
",
+ "",
+ "| ",
+ "",
+ "### Core Applications",
+ "",
+ "| Application | Status |",
+ "| :--- | :---: |"
+ ]
+
+ # Add core applications
+ for app in apps_by_category.get('core', []):
+ lines.append(generate_badge_line(app))
+
+ lines.extend([
+ "",
+ "### Games",
+ "",
+ "| Application | Status |",
+ "| :--- | :---: |"
+ ])
+
+ # Add games
+ for app in apps_by_category.get('games', []):
+ lines.append(generate_badge_line(app))
+
+ lines.extend([
+ " | ",
+ "",
+ "",
+ "### Applications",
+ "",
+ "| Application | Status |",
+ "| :--- | :---: |"
+ ])
+
+ # Add applications
+ for app in apps_by_category.get('apps', []):
+ lines.append(generate_badge_line(app))
+
+ lines.extend([
+ "",
+ " | ",
+ "
",
+ "
"
+ ])
+
+ return '\n'.join(lines) + '\n'
+
+
+def main():
+ if len(sys.argv) < 2:
+ print("Usage: generate-readme.py [output-file]")
+ print("Example: generate-readme.py k8s/ README.md")
+ sys.exit(1)
+
+ k8s_path = sys.argv[1]
+ output_file = sys.argv[2] if len(sys.argv) > 2 else "README.md"
+
+ if not os.path.exists(k8s_path):
+ print(f"Error: Directory {k8s_path} does not exist")
+ sys.exit(1)
+
+ print(f"📁 Scanning {k8s_path}...")
+ apps_by_category = scan_k8s_directory(k8s_path)
+
+ # Print statistics
+ total_apps = sum(len(apps) for apps in apps_by_category.values())
+ print(f"✅ Found {total_apps} applications:")
+ for category, apps in apps_by_category.items():
+ if apps:
+ print(f" - {category}: {len(apps)} apps")
+
+ print(f"📝 Generating {output_file}...")
+ readme_content = generate_readme_content(apps_by_category)
+
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write(readme_content)
+
+ print(f"✅ {output_file} generated successfully")
+ print(f" Total lines: {len(readme_content.splitlines())}")
+ print(f" File size: {len(readme_content)} bytes")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.gitea/workflows/update-readme.yaml b/.gitea/workflows/update-readme.yaml
new file mode 100644
index 0000000..7e6a1bd
--- /dev/null
+++ b/.gitea/workflows/update-readme.yaml
@@ -0,0 +1,156 @@
+name: 'Auto-update README'
+
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - 'k8s/**'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ update-readme:
+ name: 'Generate README and Create MR'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Configure Git
+ run: |
+ git config --global user.name "Gitea Actions Bot"
+ git config --global user.email "actions@gitea.local"
+
+ - name: Generate README
+ run: |
+ echo "📋 Starting README generation..."
+ python3 .gitea/scripts/generate-readme.py k8s/ README.md
+
+ if [ -f "README.md" ]; then
+ echo "✅ README generated successfully"
+ echo "📄 File size: $(wc -c < README.md) bytes"
+ echo "📄 Lines: $(wc -l < README.md)"
+ else
+ echo "❌ README not generated"
+ exit 1
+ fi
+
+ - name: Check for changes
+ id: check_changes
+ run: |
+ if git diff --quiet README.md; then
+ echo "No changes detected in README.md"
+ echo "has_changes=false" >> $GITHUB_OUTPUT
+ else
+ echo "Changes detected in README.md"
+ echo "has_changes=true" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Create Pull Request
+ if: steps.check_changes.outputs.has_changes == 'true'
+ run: |
+ # Set variables
+ GITEA_URL="${{ secrets.GT_URL }}"
+ GITEA_TOKEN="${{ secrets.GT_TOKEN }}"
+ GITEA_OWNER="${{ secrets.GT_OWNER }}"
+ GITEA_REPO="${{ secrets.GT_REPO }}"
+ BRANCH_NAME="auto-update-readme-$(date +%Y%m%d-%H%M%S)"
+
+ echo "🔍 Configuration:"
+ echo "GITEA_URL: ${GITEA_URL:-NOT SET}"
+ echo "GITEA_OWNER: ${GITEA_OWNER:-NOT SET}"
+ echo "GITEA_REPO: ${GITEA_REPO:-NOT SET}"
+ echo "BRANCH_NAME: $BRANCH_NAME"
+
+ # Create and push new branch
+ echo "🌿 Creating branch: $BRANCH_NAME"
+ git checkout -b "$BRANCH_NAME"
+ git add README.md
+ git commit -m "Auto-update README with current k8s applications
+
+Generated by CI/CD workflow on $(date +%Y-%m-%d\ %H:%M:%S)
+
+This PR updates the README.md file with the current list of applications
+found in the k8s/ directory structure."
+
+ # Push branch to remote
+ echo "📤 Pushing branch to remote..."
+ git push origin "$BRANCH_NAME"
+
+ # Create Pull Request using Gitea API
+ echo "🔀 Creating Pull Request..."
+
+ PR_TITLE="Auto-update README with k8s applications"
+ PR_BODY="This PR automatically updates README.md based on the current k8s/ directory structure.
+
+## Changes
+- Updated application list in README.md
+- Applications are now synced with k8s/ folders
+
+## Review
+Please review and merge if everything looks correct.
+
+---
+🤖 This PR was automatically generated by CI/CD workflow
+⏰ Generated at: $(date +%Y-%m-%d\ %H:%M:%S)"
+
+ # Create PR via API
+ RESPONSE=$(curl -s -X POST \
+ -H "Authorization: token $GITEA_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"title\": \"$PR_TITLE\",
+ \"body\": \"$PR_BODY\",
+ \"head\": \"$BRANCH_NAME\",
+ \"base\": \"main\"
+ }" \
+ "$GITEA_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/pulls")
+
+ # Extract PR number and URL from response
+ PR_NUMBER=$(echo "$RESPONSE" | grep -o '"number":[0-9]*' | head -1 | cut -d':' -f2)
+
+ if [ -n "$PR_NUMBER" ]; then
+ echo "✅ Pull Request created successfully!"
+ echo "📝 PR #$PR_NUMBER"
+ echo "🔗 URL: $GITEA_URL/$GITEA_OWNER/$GITEA_REPO/pulls/$PR_NUMBER"
+
+ # Save PR info for summary
+ echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
+ echo "pr_url=$GITEA_URL/$GITEA_OWNER/$GITEA_REPO/pulls/$PR_NUMBER" >> $GITHUB_OUTPUT
+ else
+ echo "⚠️ Failed to create Pull Request"
+ echo "Response: $RESPONSE"
+ exit 1
+ fi
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "## 📊 README Update Summary" >> $GITHUB_STEP_SUMMARY
+
+ if [ -f "README.md" ]; then
+ echo "- ✅ README generated successfully" >> $GITHUB_STEP_SUMMARY
+
+ if [ "${{ steps.check_changes.outputs.has_changes }}" = "true" ]; then
+ echo "- ✅ Changes detected" >> $GITHUB_STEP_SUMMARY
+ echo "- ✅ Pull Request created" >> $GITHUB_STEP_SUMMARY
+
+ if [ -n "${{ steps.create_pr.outputs.pr_number }}" ]; then
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**PR:** [#${{ steps.create_pr.outputs.pr_number }}](${{ steps.create_pr.outputs.pr_url }})" >> $GITHUB_STEP_SUMMARY
+ fi
+ else
+ echo "- ℹ️ No changes detected - README already up to date" >> $GITHUB_STEP_SUMMARY
+ fi
+ else
+ echo "- ❌ README generation failed" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Generated at:** $(date)" >> $GITHUB_STEP_SUMMARY
diff --git a/.gitignore b/.gitignore
index c853555..b74dc47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,8 @@ crash.*.log
*.tfvars.json
!*terraform.tfvars
+# claude ai
+.claude/
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
diff --git a/README.md b/README.md
index f258df8..8100396 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ ArgoCD homelab project
| **cert-manager** | [](https://ag.hexor.cy/applications/argocd/cert-manager) |
| **external-secrets** | [](https://ag.hexor.cy/applications/argocd/external-secrets) |
| **k3s-system-upgrade** | [](https://ag.hexor.cy/applications/argocd/k3s-system-upgrade) |
+| **kube-system-custom** | [](https://ag.hexor.cy/applications/argocd/kube-system-custom) |
| **kubernetes-dashboard** | [](https://ag.hexor.cy/applications/argocd/kubernetes-dashboard) |
| **postgresql** | [](https://ag.hexor.cy/applications/argocd/postgresql) |
| **prom-stack** | [](https://ag.hexor.cy/applications/argocd/prom-stack) |
@@ -25,7 +26,6 @@ ArgoCD homelab project
| Application | Status |
| :--- | :---: |
-| **beam-ng** | [](https://ag.hexor.cy/applications/argocd/beam-ng) |
| **counter-strike-16** | [](https://ag.hexor.cy/applications/argocd/counter-strike-16) |
| **minecraft** | [](https://ag.hexor.cy/applications/argocd/minecraft) |
@@ -36,18 +36,17 @@ ArgoCD homelab project
| Application | Status |
| :--- | :---: |
| **gitea** | [](https://ag.hexor.cy/applications/argocd/gitea) |
-| **greece-notifier** | [](https://ag.hexor.cy/applications/argocd/greece-notifier) |
| **hexound** | [](https://ag.hexor.cy/applications/argocd/hexound) |
| **immich** | [](https://ag.hexor.cy/applications/argocd/immich) |
| **jellyfin** | [](https://ag.hexor.cy/applications/argocd/jellyfin) |
| **k8s-secrets** | [](https://ag.hexor.cy/applications/argocd/k8s-secrets) |
| **khm** | [](https://ag.hexor.cy/applications/argocd/khm) |
| **paperless** | [](https://ag.hexor.cy/applications/argocd/paperless) |
+| **pasarguard** | [](https://ag.hexor.cy/applications/argocd/pasarguard) |
| **qbittorent-nas** | [](https://ag.hexor.cy/applications/argocd/qbittorent-nas) |
| **rustdesk** | [](https://ag.hexor.cy/applications/argocd/rustdesk) |
-| **sonarr-stack** | [](https://ag.hexor.cy/applications/argocd/sonarr-stack) |
-| **stirling-pdf** | [](https://ag.hexor.cy/applications/argocd/stirling-pdf) |
| **syncthing** | [](https://ag.hexor.cy/applications/argocd/syncthing) |
+| **tg-bots** | [](https://ag.hexor.cy/applications/argocd/tg-bots) |
| **vaultwarden** | [](https://ag.hexor.cy/applications/argocd/vaultwarden) |
| **vpn** | [](https://ag.hexor.cy/applications/argocd/vpn) |