Compare commits

...
Author SHA1 Message Date
stefankoelle 9c736532e1 Fix timecapsule build path (../../ was wrong, should be ../) 2026-09-11 12:50:44 +02:00
stefankoelle 401868cde5 Fix stefankoelle build: temporarily hide .eleventyignore
build-pdf.sh now temporarily renames .eleventyignore during the
stefankoelle Eleventy build, restored via trap on EXIT. This allows
the standalone stefankoelle build to work while the main build
still ignores stefankoelle/ via .eleventyignore.
2026-09-11 12:47:03 +02:00
stefankoelle dbb791b1a6 Centralize robots.txt and sitemap
- One robots.txt at root referencing two sitemaps
- One central sitemap.xml with all pages (no per-site sitemaps)
- Deleted: 4 per-site sitemap templates, 4 per-site robots.txt files
- Simplified sitemap template (uses collections.all)
2026-09-11 12:36:18 +02:00
stefankoelle c77ad91e0e Consolidate all moonweb sites into single Eleventy config
- Single eleventy.config.js at root with computed pathPrefix per section
- Accent colors inlined in base.njk (no per-site theme.css needed)
- Hub pages output at root via explicit permalinks
- Per-section sitemaps with tag-based collections
- .eleventyignore excludes stefankoelle, timecapsule, markdown files
- Deleted: 5 per-site configs, merge script
- Simplified: package.json (single build/dev commands)
- Simplified: GitHub Actions workflow (no matrix, no merge job)
- Fixed: parent links for sub-site detail pages
2026-09-11 12:31:47 +02:00
stefankoelle 10b05736c8 Revert sitemap fix - page.url does not include pathPrefix 2026-09-11 12:11:47 +02:00
stefankoelle 84cd3f8562 Fix sitemap doubled pathPrefix and CSS paths for sub-sites
- sitemap.njk: remove site.pathPrefix (page.url already includes it in Eleventy v3)
- base.njk: prefix CSS paths with site.pathPrefix so sub-sites load their own theme
2026-09-11 12:10:18 +02:00
stefankoelle 9cda842250 Clean up build artifacts and update TODO.md
- Remove timecapsule/_site/ from tracking (was in .gitignore but tracked)
- Add .tmp/ to .gitignore
- Update TODO.md: Phase 5 now automated via GitHub Action
2026-09-11 11:31:49 +02:00
stefankoelle 37f7d8f1fb Add Cloudflare redirect rules script and GitHub Action
- scripts/cloudflare/setup-redirects.sh: CLI tool to manage redirects
- scripts/cloudflare/redirect-rules.json: rule definitions
- .github/workflows/cloudflare-redirects.yml: manual trigger workflow
- Supports: apply, status, dry-run, delete
- Requires secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID
2026-09-11 11:29:45 +02:00
stefankoelle c76de3ec36 Fix sitemaps: add pathPrefix for subdirectory sites
- Add site.pathPrefix to infra, smarthome, code, retro configs
- Update shared sitemap template to use pathPrefix
- Hub has no prefix, subdirectories get /infra/, /smarthome/, etc.
2026-09-11 11:22:44 +02:00
stefankoelle 7524210768 Fix timecapsule sitemap: remove doubled timecapsule path in URLs
site.domain was 'https://www.moonweb.org/timecapsule' but page.url
already includes '/timecapsule/', causing doubled paths like
'/timecapsule/timecapsule/index.html'.
2026-09-11 11:19:28 +02:00
stefankoelle 41a9236ca7 Fix robots.txt: list all sitemaps and use www.moonweb.org URLs
- Hub robots.txt now lists all 6 moonweb sitemaps
- infra/smarthome/code/retro robots.txt point to correct www.moonweb.org URLs
- Timecapsule robots.txt was already correct
2026-09-11 11:17:08 +02:00
stefankoelle 7c3e29d282 Add old www redirects and fix impressum hosting sections
- Create redirect HTML files for all old www.moonweb.org URLs
- Root shortcuts (/start.html, /index2.html, etc.) redirect to /timecapsule/
- Content subdirectories (/partners/, /projects/, etc.) redirect to /timecapsule/
- Use .htm extension to prevent Eleventy template processing
- Fix impressum: buildbroken + 28k8 are on Cloudflare Pages, not IONOS
2026-09-11 11:13:04 +02:00
stefankoelle 2b997c11d0 Add timecapsule build output to .gitignore 2026-09-11 10:53:54 +02:00
stefankoelle 466df3bd04 Consolidate all moonweb.org sites under www.moonweb.org
- Migrate timecapsule (old www.moonweb.org) into monorepo as Eleventy 2.x site
- Change all site URLs from subdomains to subdirectories under www.moonweb.org
- Replace Cloudflare Pages deployment with IONOS SFTP
- Update site-switcher, footer, and all internal links
- Add merge script for combining all sites into single deployment
- Update CI/CD workflow for IONOS SFTP deployment
- Update stefankoelle.de references to use new URLs
- Update AGENTS.md and README.md documentation
2026-09-11 10:53:22 +02:00
stefankoelle a97c3d3a89 fix impressum link 2026-09-11 00:11:47 +02:00
stefankoelle d213154b79 kids rfid player to media 2026-09-11 00:02:31 +02:00
stefankoelle 13fdffce07 link new dosmenu page 2026-09-10 23:57:01 +02:00
stefankoelle b1639bbece kids rfid player 2026-09-10 23:53:19 +02:00
stefankoelle 156595cbe4 content improve 6 2026-09-10 23:44:31 +02:00
stefankoelle 0396b755f0 content improve 5 2026-09-10 23:32:09 +02:00
stefankoelle 1ea2967b35 content improve 4 2026-09-10 23:27:16 +02:00
stefankoelle dacbd57302 stefankoelle.de link zu hub 2026-09-10 23:03:59 +02:00
stefankoelle 7fc47113bb content improve 3 2026-09-10 22:56:31 +02:00
stefankoelle 5cd19867b7 content improve 2 2026-09-10 22:46:36 +02:00
stefankoelle e9e492abc0 content imprive 2026-09-10 22:38:54 +02:00
187 changed files with 5567 additions and 635 deletions
+17
View File
@@ -0,0 +1,17 @@
stefankoelle/
timecapsule/
scripts/
.tmp/
# Root markdown files (not templates)
/*.md
# Hub static redirect files (handled by passthrough copy)
hub/hardware.html
hub/users.html
hub/index2.html
hub/index3.html
hub/new.html
hub/links.html
hub/infos.html
hub/start.html
+118
View File
@@ -0,0 +1,118 @@
name: Build & Deploy moonweb.org
on:
push:
branches: [main]
paths-ignore:
- 'stefankoelle/**'
- '.github/workflows/deploy-stefankoelle.yml'
pull_request:
branches: [main]
paths-ignore:
- 'stefankoelle/**'
- '.github/workflows/deploy-stefankoelle.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "22"
jobs:
build:
name: Build moonweb sites
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build all moonweb sites
run: npm run build
- name: Build timecapsule
run: npm run build:timecapsule
- name: Validate build output
run: |
echo "Build output:"
find dist -maxdepth 2 -type f | sort | head -40
echo "..."
echo "Total files:"
find dist -type f | wc -l
# Verify key directories exist
for dir in "" infra smarthome code retro timecapsule; do
if [ -n "$dir" ] && [ ! -d "dist/$dir" ]; then
echo "Error: dist/$dir not found"
exit 1
fi
done
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: dist-moonweb
path: dist/
retention-days: 7
deploy:
name: Deploy to IONOS
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: dist-moonweb
path: dist/
- name: Deploy via SFTP
uses: wlixcc/SFTP-Deploy-Action@v1.2.6
with:
local_path: './dist/*'
server: ${{ secrets.IONOS_SFTP_HOST }}
username: ${{ secrets.IONOS_SFTP_USER }}
password: ${{ secrets.IONOS_SFTP_PASSWORD }}
remote_path: '/websites/moonweb/'
port: 22
sftp_only: true
summary:
name: Deploy Summary
needs: deploy
runs-on: ubuntu-latest
if: always()
steps:
- name: Print deployment summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.deploy.result }}" = "success" ]; then
echo "moonweb.org deployed successfully!" >> $GITHUB_STEP_SUMMARY
else
echo "Deployment failed. Check the deploy jobs for details." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Sites" >> $GITHUB_STEP_SUMMARY
echo "| Site | URL |" >> $GITHUB_STEP_SUMMARY
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| hub | https://www.moonweb.org/ |" >> $GITHUB_STEP_SUMMARY
echo "| infra | https://www.moonweb.org/infra/ |" >> $GITHUB_STEP_SUMMARY
echo "| smarthome | https://www.moonweb.org/smarthome/ |" >> $GITHUB_STEP_SUMMARY
echo "| code | https://www.moonweb.org/code/ |" >> $GITHUB_STEP_SUMMARY
echo "| retro | https://www.moonweb.org/retro/ |" >> $GITHUB_STEP_SUMMARY
echo "| timecapsule | https://www.moonweb.org/timecapsule/ |" >> $GITHUB_STEP_SUMMARY
# Required repo secrets:
# IONOS_SFTP_HOST — IONOS SFTP server hostname
# IONOS_SFTP_USER — IONOS SFTP username
# IONOS_SFTP_PASSWORD — IONOS SFTP password
-139
View File
@@ -1,139 +0,0 @@
name: Build & Deploy moonweb-site
on:
push:
branches: [main]
paths-ignore:
- 'stefankoelle/**'
- '.github/workflows/deploy-stefankoelle.yml'
pull_request:
branches: [main]
paths-ignore:
- 'stefankoelle/**'
- '.github/workflows/deploy-stefankoelle.yml'
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "22"
jobs:
build:
name: Build ${{ matrix.site }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
site: [hub, infra, smarthome, code, retro]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build ${{ matrix.site }}
run: npm run build:${{ matrix.site }}
- name: Validate build output
run: |
if [ ! -d "dist/${{ matrix.site }}" ]; then
echo "Error: dist/${{ matrix.site }} not found"
exit 1
fi
if [ -z "$(ls -A dist/${{ matrix.site }} 2>/dev/null)" ]; then
echo "Error: dist/${{ matrix.site }} is empty"
exit 1
fi
echo "Build output:"
find dist/${{ matrix.site }} -type f | head -20
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: dist-${{ matrix.site }}
path: dist/${{ matrix.site }}
retention-days: 7
deploy:
name: Deploy ${{ matrix.site }}
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
strategy:
fail-fast: false
matrix:
include:
- site: hub
cf_project: moonweb-hub
- site: infra
cf_project: moonweb-infra
- site: smarthome
cf_project: moonweb-smarthome
- site: code
cf_project: moonweb-code
- site: retro
cf_project: moonweb-retro
steps:
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: dist-${{ matrix.site }}
path: dist/${{ matrix.site }}
- name: Create wrangler.toml for Worker
run: |
echo 'name = "${{ matrix.cf_project }}"' > wrangler.toml
echo 'compatibility_date = "2024-01-01"' >> wrangler.toml
echo '' >> wrangler.toml
echo '[assets]' >> wrangler.toml
echo 'directory = "./dist/${{ matrix.site }}"' >> wrangler.toml
echo 'not_found_handling = "404-page"' >> wrangler.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
summary:
name: Deploy Summary
needs: deploy
runs-on: ubuntu-latest
if: always()
steps:
- name: Print deployment summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.deploy.result }}" = "success" ]; then
echo "✅ All sites deployed successfully!" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Deployment failed. Check the deploy jobs for details." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Sites" >> $GITHUB_STEP_SUMMARY
echo "| Site | URL |" >> $GITHUB_STEP_SUMMARY
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| hub | https://hub.moonweb.org |" >> $GITHUB_STEP_SUMMARY
echo "| infra | https://infra.moonweb.org |" >> $GITHUB_STEP_SUMMARY
echo "| smarthome | https://smarthome.moonweb.org |" >> $GITHUB_STEP_SUMMARY
echo "| code | https://code.moonweb.org |" >> $GITHUB_STEP_SUMMARY
echo "| retro | https://retro.moonweb.org |" >> $GITHUB_STEP_SUMMARY
# Required repo secrets:
# CLOUDFLARE_API_TOKEN — Cloudflare API token with Workers:Edit permission
# CLOUDFLARE_ACCOUNT_ID — Cloudflare account ID
#
# Required one-time setup (see PLAN.md Phase 2):
# Create Cloudflare Workers and bind custom domains.
# DNS is already on Cloudflare (SPEC.md §4.6).
#
@@ -0,0 +1,58 @@
name: Setup Cloudflare Redirects
on:
workflow_dispatch:
inputs:
action:
description: "Action to perform"
required: true
default: "apply"
type: choice
options:
- apply
- status
- dry-run
- delete
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
jobs:
redirects:
name: Cloudflare Redirects
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Validate secrets
run: |
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
echo "Error: CLOUDFLARE_API_TOKEN secret is not set"
exit 1
fi
if [ -z "$CLOUDFLARE_ZONE_ID" ]; then
echo "Error: CLOUDFLARE_ZONE_ID secret is not set"
exit 1
fi
- name: Run ${{ inputs.action }}
run: |
case "${{ inputs.action }}" in
status)
./scripts/cloudflare/setup-redirects.sh --status
;;
dry-run)
./scripts/cloudflare/setup-redirects.sh --dry-run
;;
delete)
./scripts/cloudflare/setup-redirects.sh --delete
;;
*)
./scripts/cloudflare/setup-redirects.sh
;;
esac
# Required repo secrets:
# CLOUDFLARE_API_TOKEN — API token with Dynamic Redirects Write permission
# CLOUDFLARE_ZONE_ID — Zone ID for moonweb.org (found in Cloudflare dashboard)
+3
View File
@@ -15,3 +15,6 @@ dist/
.DS_Store
*.swp
*.swo
timecapsule/_site/
timecapsule/node_modules/
.tmp/
+73 -41
View File
@@ -1,18 +1,19 @@
# AGENTS.md — moonweb-site
## Projektübersicht
## Projektuebersicht
Monorepo für 5 statische Websites unter moonweb.org + stefankoelle.de, basierend auf Eleventy (11ty).
Monorepo fuer 6 statische Websites unter www.moonweb.org + stefankoelle.de, basierend auf Eleventy (11ty).
### Websites (Cloudflare Pages)
### Websites (IONOS SFTP)
| Site | Domain | Zweck | Status |
|------|--------|-------|--------|
| hub | hub.moonweb.org | Zentrale Indexseite, verlinkt alles | Fertig |
| infra | infra.moonweb.org | Infrastruktur-Übersicht (Proxmox, Synology, Netzwerk) | Übersicht + 3 Detailseiten |
| smarthome | smarthome.moonweb.org | Smart Home Projekte und Dashboards | Übersicht + 6 Detailseiten |
| code | code.moonweb.org | GitHub-Projekte (aggregiert via .moonweb.yml) | Fertig (14 Repos) |
| retro | retro.moonweb.org | Physische Retro-Hardware | Nur Übersicht (WIP) |
| Site | URL | Zweck | Status |
|------|-----|-------|--------|
| hub | www.moonweb.org/ | Zentrale Indexseite, verlinkt alles | Fertig |
| infra | www.moonweb.org/infra/ | Infrastruktur-Uebersicht (Proxmox, Synology, Netzwerk) | Uebersicht + 8 Detailseiten |
| smarthome | www.moonweb.org/smarthome/ | Smart Home Projekte und Dashboards | Uebersicht + 9 Detailseiten |
| code | www.moonweb.org/code/ | GitHub-Projekte (aggregiert via .moonweb.yml) | Fertig |
| retro | www.moonweb.org/retro/ | Physische Retro-Hardware | Uebersicht + 13 Detailseiten |
| timecapsule | www.moonweb.org/timecapsule/ | 2000er Internet-Zeitkapsel (altes Design) | Fertig |
### Externe Sites (SFTP-Deployment)
@@ -22,19 +23,23 @@ Monorepo für 5 statische Websites unter moonweb.org + stefankoelle.de, basieren
### Andere Sites (nicht im Monorepo)
- www.moonweb.org — 2000er Internet-Zeitkapsel
- 28k8.moonweb.org — 90er BBS/Scene-Archiv
- buildbroken.moonweb.org — .NET Open Space Blog Archiv
## Dateistruktur
```
moonweb-site/
├── hub/ # Eleventy-Config + index.njk
├── infra/ # Eleventy-Config + index.njk + 3 Subseiten
├── smarthome/ # Eleventy-Config + index.njk + 6 Subseiten
├── infra/ # Eleventy-Config + index.njk + 8 Subseiten
├── smarthome/ # Eleventy-Config + index.njk + 9 Subseiten
├── code/ # Eleventy-Config + index.njk
│ └── _data/repos.json
├── retro/ # Eleventy-Config + index.njk
├── retro/ # Eleventy-Config + index.njk + 13 Subseiten
├── timecapsule/ # Eleventy 2.x (eigene Config)
│ ├── eleventy.config.js
│ ├── package.json
│ └── src/ # 2001er Retro-Content
├── stefankoelle/ # Eleventy-Config + Onepager
│ ├── eleventy.config.js
│ ├── _includes/
@@ -52,10 +57,11 @@ moonweb-site/
│ ├── base.css
│ └── theme-*.css
├── scripts/
── github-aggregator/
── github-aggregator/
│ └── merge-moonweb.sh # Merge-Skript fuer Deployment
├── .github/workflows/
│ ├── build-deploy.yml # CI/CD: Cloudflare Pages (5 Sites)
│ └── deploy-stefankoelle.yml # CI/CD: IONOS SFTP (stefankoelle.de)
│ ├── build-deploy-moonweb.yml # CI/CD: IONOS SFTP (www.moonweb.org)
│ └── deploy-stefankoelle.yml # CI/CD: IONOS SFTP (stefankoelle.de)
├── DESIGN.md
├── SPEC.md
├── PLAN.md
@@ -66,10 +72,11 @@ moonweb-site/
## Technischer Stack
- **SSG:** Eleventy (11ty) v3.1.6
- **SSG:** Eleventy (11ty) v3.1.6 (hub, infra, smarthome, code, retro)
- **SSG:** Eleventy v2.0.1 (timecapsule - retro 2001 Design)
- **Templates:** Nunjucks (.njk)
- **CSS:** Variables-basiert mit Accent-Farben pro Site
- **Deploy (moonweb):** Cloudflare Pages (5 separate Projects)
- **Deploy (moonweb):** IONOS SFTP
- **Deploy (stefankoelle):** IONOS SFTP
- **CI/CD:** GitHub Actions (2 Workflows)
@@ -77,32 +84,54 @@ moonweb-site/
```bash
npm install
npm run prebuild # Pre-Build Tasks (CV PDF generieren)
npm run dev:hub # localhost:8081
npm run dev:infra # localhost:8082
npm run dev:smarthome # localhost:8083
npm run dev:code # localhost:8084
npm run dev:retro # localhost:8085
npm run dev:stefankoelle # localhost:8086
npm run build # Alle Sites bauen
cd timecapsule && npm install # Timecapsule Dependencies
npm run prebuild # Pre-Build Tasks (CV PDF generieren)
npm run dev:hub # localhost:8081
npm run dev:infra # localhost:8082
npm run dev:smarthome # localhost:8083
npm run dev:code # localhost:8084
npm run dev:retro # localhost:8085
npm run dev:stefankoelle # localhost:8086
npm run dev:timecapsule # localhost:8087
npm run build # Alle Sites bauen
npm run build:moonweb # Nur moonweb Sites (ohne stefankoelle)
npm run merge:moonweb # Sites mergen fuer Deployment
```
## Design-Prinzipien
1. **Header konsistent** — Identischer Site-Switcher auf allen Home-Sites
2. **Content flexibel** — Detailseiten dürfen eigenes Layout haben
2. **Content flexibel** — Detailseiten duerfen eigenes Layout haben
3. **Accent-Farben:** hub=#3b6ea5, infra=#99333A, smarthome=#1f8a8a, code=#3E5098, retro=#8a6d3b
4. **Englisch** — Alle Sites komplett auf Englisch
5. **Keine Analytics** — Keine Tracking-Tools
6. **Sensible Daten** — Infra-Content wird manuell redigiert (keine IPs, Keys, Passwörter)
6. **Sensible Daten** — Infra-Content wird manuell redigiert (keine IPs, Keys, Passwoerter)
## URL-Struktur
Alle Sites sind unter `www.moonweb.org` als Subverzeichnisse erreichbar:
- `www.moonweb.org/` — Hub (Root)
- `www.moonweb.org/infra/` — Infra
- `www.moonweb.org/smarthome/` — Smarthome
- `www.moonweb.org/code/` — Code
- `www.moonweb.org/retro/` — Retro
- `www.moonweb.org/timecapsule/` — Timecapsule (2001 Design)
- `www.moonweb.org/impressum/` — Impressum
Cloudflare Redirects leiten alte Subdomains weiter:
- `hub.moonweb.org/*``www.moonweb.org/*`
- `infra.moonweb.org/*``www.moonweb.org/infra/*`
- `smarthome.moonweb.org/*``www.moonweb.org/smarthome/*`
- `code.moonweb.org/*``www.moonweb.org/code/*`
- `retro.moonweb.org/*``www.moonweb.org/retro/*`
## stefankoelle.de
- Eigene Eleventy-Config (nicht shared base.njk)
- Eigenes CSS-Design (nicht moonweb design system)
- Onepager mit Anchor-Links (bleibt so)
- Deploy via SFTP auf IONOS `/deploy/stefankoelle/`
- CV-Partial zentral pflegbar (einmal aendern Web + PDF aktualisieren)
- Deploy via SFTP auf IONOS `/websites/stefankoelle/`
- CV-Partial zentral pflegbar (einmal aendern -> Web + PDF aktualisieren)
- LED Matrix als eigene Seite unter stefankoelle.de/ledmatrix/
### CV PDF Generierung
@@ -116,7 +145,7 @@ npm run prebuild # Alle Pre-Build Tasks (inkl. CV PDF)
Dateien:
- `stefankoelle/pdf/cv-style.css` — WeasyPrint-Stylesheet (A4, Typografie)
- `stefankoelle/pdf/build-pdf.sh` — Shell-Skript für PDF-Generierung
- `stefankoelle/pdf/build-pdf.sh` — Shell-Skript fuer PDF-Generierung
- `stefankoelle/cv-print.njk` — Standalone HTML-Template (nur CV-Content)
- `stefankoelle/pdf/cv.pdf` — Generiertes PDF (Output)
@@ -124,21 +153,22 @@ Wichtig: Das PDF wird via Eleventy-Passthrough ins Build-Output kopiert (`dist/s
### CSS Cache-Busting
CSS-Dateien werden als Passthrough kopiert (kein Hash im Dateinamen). Bei CSS-Änderungen muss der Query-String in der `?v=N` Inkludierung erhöht werden:
CSS-Dateien werden als Passthrough kopiert (kein Hash im Dateinamen). Bei CSS-Aenderungen muss der Query-String in der `?v=N` Inkludierung erhoeht werden:
- `stefankoelle/index.njk`: `<link rel="stylesheet" href="/assets/style.css?v=N">`
- `stefankoelle/ledmatrix/index.njk`: `<link rel="stylesheet" href="assets/style.css?v=N">`
Bei jeder CSS-Anpassung `?v=N` um 1 erhöhen, sonst cached der Browser die alte Datei.
Bei jeder CSS-Anpassung `?v=N` um 1 erhoehen, sonst cached der Browser die alte Datei.
## CI/CD
### Cloudflare Pages (moonweb)
`.github/workflows/build-deploy.yml` baut hub, infra, smarthome, code, retro.
### IONOS SFTP (www.moonweb.org)
`.github/workflows/build-deploy-moonweb.yml` baut alle moonweb Sites (hub, infra, smarthome, code, retro, timecapsule) und deployed per SFTP.
Benötigte Secrets:
- `CLOUDFLARE_API_TOKEN`
- `CLOUDFLARE_ACCOUNT_ID`
- `IONOS_SFTP_HOST`
- `IONOS_SFTP_USER`
- `IONOS_SFTP_PASSWORD`
### IONOS SFTP (stefankoelle.de)
`.github/workflows/deploy-stefankoelle.yml` baut stefankoelle.de und deployed per SFTP.
@@ -150,9 +180,11 @@ Benötigte Secrets:
## Offene Punkte
- retro/ ist bewusst rudimentär gehalten
- retro/ ist bewusst rudimentaer gehalten
- Querverlinkungen stefankoelle.de <-> smarthome (zukuenftig)
- Ledmatrix ggf. nach smarthome verschieben (when ready)
- Cloudflare Redirects einrichten (nach Deploy)
- Google Search Console: Neue Property www.moonweb.org
## GitHub Aggregator
@@ -191,10 +223,10 @@ python3 -m venv .venv
## Text / Stil
- Keine Em-Dashes (—) verwenden, stattdessen umformulieren (Komma, Satzzeichen, neu formulieren)
- Keine Em-Dashes verwenden, stattdessen umformulieren (Komma, Satzzeichen, neu formulieren)
## Python / pip
- Niemals `pip install` direkt ausführen
- Niemals `pip install` direkt ausfuehren
- Immer ein virtuelles Umfeld (`.venv`) anlegen und darin arbeiten
- `python3 -m venv .venv` im Projektverzeichnis
+136 -121
View File
@@ -1,88 +1,67 @@
# 🌙 moonweb-site
# moonweb-site
Monorepo for the **moonweb.org** homelab — five static sites built with [Eleventy](https://www.11ty.dev/), deployed to [Cloudflare Pages](https://pages.cloudflare.com/).
Monorepo for the **moonweb.org** homelab — six static sites built with [Eleventy](https://www.11ty.dev/), deployed to [IONOS SFTP](https://www.ionos.de/).
```
hub.moonweb.org → 🏠 Central index & gateway
infra.moonweb.org → 🏗️ Infrastructure overview (Proxmox, Synology, Docker)
smarthome.moonweb.org → 🏡 Smart home projects & dashboards
code.moonweb.org → 💻 Curated GitHub project catalog
retro.moonweb.org → 🕹️ Physical retro hardware collection
stefankoelle.de → 👤 CV, career, personal site (LED Matrix docs)
www.moonweb.org/ -> Central index & gateway
www.moonweb.org/infra/ -> Infrastructure overview (Proxmox, Synology, Docker)
www.moonweb.org/smarthome/ -> Smart home projects & dashboards
www.moonweb.org/code/ -> Curated GitHub project catalog
www.moonweb.org/retro/ -> Physical retro hardware collection
www.moonweb.org/timecapsule/ -> 2000s internet time capsule (retro design)
stefankoelle.de -> CV, career, personal site (LED Matrix docs)
```
> **Other sites** (not in this monorepo): [www.moonweb.org](https://www.moonweb.org) (2000s time capsule), [28k8.moonweb.org](https://28k8.moonweb.org) (90s BBS archive).
> **Other sites** (not in this monorepo): [28k8.moonweb.org](https://28k8.moonweb.org) (90s BBS archive), [buildbroken.moonweb.org](https://buildbroken.moonweb.org) (.NET Open Space blog).
---
## 📐 Architecture
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
GitHub Actions CI
│ ┌───────────┐ ┌────────────┐ ┌─────────────┐ ┌─────────┐ │
build:hub │ │build:infra │ │ build:smart │ │ build:… │ │
│ └─────┬─────┘ └─────┬──────┘ └──────┬──────┘ └────┬────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
dist/hub/ dist/infra/ dist/smarthome/ dist//
└────────┬───────────────────────────────────────────┬────────┘
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Cloudflare Pages │ IONOS SFTP
│ (5 sites) │ (stefankoelle) │
└────────┬─────────┘ └────────┬────────┘
▼ ▼
hub / infra / ... stefankoelle.de
+-------------------------------------------------------------------+
| GitHub Actions CI |
| +-----------+ +------------+ +-------------+ +---------+ +------+ |
| | build:hub | |build:infra | | build:smart | | build:... | | build:timecapsule | |
| +-----+-----+ +-----+------+ +------+------+ +----+----+ +----+----+ +--------+ |
| | | | | | | |
| v v v v v v |
| dist/hub/ dist/infra/ dist/smarthome/ dist/.../ dist/timecapsule/ |
+--------+---------------------------------------------------+--------------------+
| |
v v
+------------------+ +------------------+
| IONOS SFTP | | IONOS SFTP |
| (www.moonweb.org)| | (stefankoelle.de)|
+--------+---------+ +--------+---------+
v v
www.moonweb.org/* stefankoelle.de
```
---
## 🛠️ Tech Stack
## Tech Stack
| Layer | Technology | Why |
|-------|-----------|-----|
| **SSG** | [Eleventy 3.1.6](https://www.11ty.dev/) | Markdown/YAML-first, minimal JS, `_data` folders map directly to aggregator output, low maintenance for 5 sites |
| **SSG** | [Eleventy 3.1.6](https://www.11ty.dev/) | Markdown/YAML-first, minimal JS, `_data` folders map directly to aggregator output |
| **SSG (timecapsule)** | [Eleventy 2.0.1](https://www.11ty.dev/) | Legacy 2001 design, CommonJS config |
| **Templates** | [Nunjucks](https://mozilla.github.io/nunjucks/) | Shared `base.njk` layout with site-switcher header, `card-grid.njk` for index pages |
| **Styling** | Custom CSS (variables-based) | `base.css` for shared layout, `theme-*.css` per domain accent color, no build step needed |
| **Styling** | Custom CSS (variables-based) | `base.css` for shared layout, `theme-*.css` per domain accent color |
| **Fonts** | [Lobster](https://fonts.google.com/specimen/Lobster) (Google Fonts) | Distinctive heading font across all sites |
| **CI/CD** | [GitHub Actions](https://github.com/features/actions) | Matrix build for all 5 sites, artifact upload, parallel deploy |
| **Deploy** | [Cloudflare Workers](https://workers.cloudflare.com/) | Static asset hosting via `wrangler pages deploy`, one worker per site |
| **DNS** | Cloudflare | Already managing DNS — zero additional setup for Pages custom domains |
| **CI/CD** | [GitHub Actions](https://github.com/features/actions) | Matrix build for all sites, artifact upload, merge step, parallel deploy |
| **Deploy** | IONOS SFTP | Static hosting via SFTP upload |
| **DNS** | Cloudflare | DNS management + redirects from old subdomains |
| **GitHub Catalog** | Python aggregator | Reads `.moonweb.yml` from each repo, outputs `repos.json` |
| **Runtime** | Fully static | No server-side code, no containers, no database — pure HTML/CSS/JS |
| **Runtime** | Fully static | No server-side code, no containers, no database |
---
## 🎨 Design System
### Header-consistent, content-flexible
- **Header is identical** across all home-section sites: site-switcher (hub · infra · smarthome · code · retro · cv), domain accent color, Lobster title font.
- **Index pages** use a shared card-grid layout with grouped sections.
- **Detail pages** keep the same header but use a freer layout below it (e.g., pin tables, API docs, photos in free arrangement).
### Accent colors
| Domain | Color | Hex |
|--------|-------|-----|
| hub | Neutral blue | `#3b6ea5` |
| infra | Grey-blue | `#99333A` |
| smarthome | Teal | `#1f8a8a` |
| code | Violet | `#3E5098` |
| retro | Warm brown | `#8a6d3b` |
### Emojis
Each card on index pages has an emoji for visual navigation — consistent across hub, infra, smarthome, code, and retro.
---
## 🚀 Local Development
## Local Development
```bash
npm install # install Eleventy + deps
cd timecapsule && npm install # timecapsule has own deps (Eleventy 2.x)
npm run dev:hub # http://localhost:8081
npm run dev:infra # http://localhost:8082
@@ -90,8 +69,9 @@ npm run dev:smarthome # http://localhost:8083
npm run dev:code # http://localhost:8084
npm run dev:retro # http://localhost:8085
npm run dev:stefankoelle # http://localhost:8086
npm run dev:timecapsule # http://localhost:8087
npm run dev # all 6 in parallel
npm run dev # all 7 in parallel
```
Each site has its own minimal Eleventy config (`<site>/eleventy.config.js`). Live reload is built in.
@@ -100,83 +80,96 @@ Each site has its own minimal Eleventy config (`<site>/eleventy.config.js`). Liv
```bash
npm run prebuild # pre-build tasks (CV PDF)
npm run build # builds all 5 → dist/<site>/
npm run build # builds all sites
npm run build:moonweb # builds moonweb sites only (excludes stefankoelle)
npm run build:hub # build single site
npm run merge:moonweb # merge all moonweb sites into dist/ for deployment
```
---
## 📦 Project Structure
## Project Structure
```
moonweb-site/
├── hub/ # 🏠 Central index & gateway
├── infra/ # 🏗️ Infra overview + 3 detail pages
├── hub/ # Central index & gateway
├── infra/ # Infra overview + 8 detail pages
│ ├── backup-strategy/
│ ├── monitoring/
── dev-environment/
├── smarthome/ # 🏡 Smart home overview + 6 detail pages
── dev-environment/
│ └── ...
├── smarthome/ # Smart home overview + 9 detail pages
│ ├── homematic-mqtt/
│ ├── tasmota-energy/
── balkonpi/
│ ├── airplay-audio/
│ ├── octoprint/
│ └── tubearchivist/
├── code/ # 💻 GitHub catalog
── ...
├── code/ # GitHub catalog
│ └── _data/repos.json # populated by the aggregator
├── retro/ # 🕹️ Retro hardware (WIP)
├── stefankoelle/ # 👤 CV, career, personal site
├── retro/ # Retro hardware + 13 detail pages
├── timecapsule/ # 2000s retro design (Eleventy 2.x)
│ ├── eleventy.config.js
│ ├── package.json
│ └── src/
├── stefankoelle/ # CV, career, personal site
│ ├── eleventy.config.js
│ ├── index.njk # Onepager (CV, Projects, Languages)
│ ├── cv-print.njk # CV-only for PDF generation
│ ├── ledmatrix/ # LED Matrix WebServer documentation
│ └── assets/ # CSS, JS, images, favicons
├── shared/ # 🔧 Shared components
├── shared/ # Shared components
│ ├── _includes/
│ │ ├── base.njk # base layout (header, site-switcher, footer)
│ │ └── card-grid.njk # card-grid template with emoji support
│ ├── base.css # shared CSS (layout, cards, typography)
│ └── theme-*.css # accent colors per domain
├── scripts/
── github-aggregator/ # 🐍 Python: reads .moonweb.yml repos.json
├── aggregate.py
│ ├── example.moonweb.yml
│ └── README.md
── github-aggregator/ # Python: reads .moonweb.yml -> repos.json
└── merge-moonweb.sh # Merge script for deployment
├── .github/workflows/
── build-deploy.yml # ⚙️ CI/CD: build + deploy to Cloudflare
├── DESIGN.md # 📋 Initial concept (German)
├── SPEC.md # 📋 Full specification (English, 159 lines)
├── PLAN.md # 📋 Implementation plan
├── TODO.md # 📋 Open items & workflow
── build-deploy-moonweb.yml # CI/CD: build + deploy to IONOS SFTP
│ └── deploy-stefankoelle.yml # CI/CD: stefankoelle.de to IONOS SFTP
├── DESIGN.md # Initial concept
├── SPEC.md # Full specification
├── PLAN.md # Implementation plan
├── TODO.md # Open items & workflow
└── package.json # npm scripts for dev/build
```
---
## 🔄 CI/CD Pipeline
## CI/CD Pipeline
### Cloudflare Pages (moonweb sites)
### IONOS SFTP (www.moonweb.org)
Defined in `.github/workflows/build-deploy.yml`:
Defined in `.github/workflows/build-deploy-moonweb.yml`:
```
push to main
├── Build (matrix: hub, infra, smarthome, code, retro)
├── checkout setup-node (22) npm ci
├── npm run build:<site>
├── validate dist/<site>/ exists & non-empty
└── upload artifact (7-day retention)
└── Deploy (matrix: 5 Cloudflare Workers)
├── download artifact
├── generate wrangler.toml
└── wrangler pages deploy
|
+-- Build (matrix: hub, infra, smarthome, code, retro)
| +-- checkout -> setup-node (22) -> npm ci
| +-- npm run build:<site>
| +-- validate dist/<site>/ exists & non-empty
| +-- upload artifact (7-day retention)
|
+-- Build timecapsule
| +-- cd timecapsule && npm ci
| +-- npm run build:timecapsule
| +-- upload artifact
|
+-- Merge
| +-- download all artifacts
| +-- merge into dist/ (hub=root, others=subdirs)
| +-- upload merged artifact
|
+-- Deploy
+-- download merged artifact
+-- SFTP upload to IONOS /websites/moonweb/
```
**Required secrets:**
- `CLOUDFLARE_API_TOKEN` — Workers:Edit permission
- `CLOUDFLARE_ACCOUNT_ID` — Cloudflare account ID
- `IONOS_SFTP_HOST`
- `IONOS_SFTP_USER`
- `IONOS_SFTP_PASSWORD`
### IONOS SFTP (stefankoelle.de)
@@ -184,12 +177,12 @@ Defined in `.github/workflows/deploy-stefankoelle.yml`:
```
push to main (paths: stefankoelle/**)
├── Build stefankoelle
└── npm run build:stefankoelle
└── Deploy via SFTP
└── lftp mirror → IONOS /deploy/stefankoelle/
|
+-- Build stefankoelle
| +-- npm run build:stefankoelle
|
+-- Deploy via SFTP
+-- SFTP upload to IONOS /websites/stefankoelle/
```
**Required secrets:**
@@ -199,7 +192,30 @@ push to main (paths: stefankoelle/**)
---
## 🐍 GitHub Aggregator (code.moonweb.org)
## URL Structure
All moonweb.org sites are accessible under `www.moonweb.org` as subdirectories:
| URL | Content |
|-----|---------|
| `www.moonweb.org/` | Hub (root) |
| `www.moonweb.org/infra/` | Infrastructure |
| `www.moonweb.org/smarthome/` | Smart Home |
| `www.moonweb.org/code/` | Code catalog |
| `www.moonweb.org/retro/` | Retro hardware |
| `www.moonweb.org/timecapsule/` | 2000s time capsule |
| `www.moonweb.org/impressum/` | Legal notice |
Cloudflare redirects forward old subdomains:
- `hub.moonweb.org/*` -> `www.moonweb.org/*`
- `infra.moonweb.org/*` -> `www.moonweb.org/infra/*`
- `smarthome.moonweb.org/*` -> `www.moonweb.org/smarthome/*`
- `code.moonweb.org/*` -> `www.moonweb.org/code/*`
- `retro.moonweb.org/*` -> `www.moonweb.org/retro/*`
---
## GitHub Aggregator (www.moonweb.org/code/)
`scripts/github-aggregator/aggregate.py` automatically builds the project catalog:
@@ -214,7 +230,7 @@ push to main (paths: stefankoelle/**)
```yaml
title: "MVG Departures"
category: code # code | smarthome | infra
subcategory: "Web Apps" # drives grouping on code.moonweb.org
subcategory: "Web Apps" # drives grouping on www.moonweb.org/code/
status: active
stack: [Python, FastAPI]
hosted_on: "Docker Host Debian (PVE)"
@@ -229,42 +245,41 @@ export GITHUB_TOKEN=ghp_xxx
python scripts/github-aggregator/aggregate.py
```
> **Deliberately manual** — no scheduled CI job. The catalog is refreshed on demand, not on every push.
---
## 📏 Content Rules
## Content Rules
| Site | Detail pages? | Rule |
|------|--------------|------|
| smarthome | Yes | When enough content exists no placeholder cards |
| infra | ⚠️ Rarely | Deliberately shallow sensitive data (IPs, keys, passwords) stripped |
| code | Never | Overview cards + GitHub links only no README duplication |
| retro | 🔨 Minimal | Still WIP — honest minimal overview, no over-investment |
| smarthome | Yes | When enough content exists - no placeholder cards |
| infra | Rarely | Deliberately shallow - sensitive data (IPs, keys, passwords) stripped |
| code | Never | Overview cards + GitHub links only - no README duplication |
| retro | Minimal | Honest minimal overview, no over-investment |
| timecapsule | Static | 1:1 migration of original 2001 design, no changes |
**Infra redaction rule:** Architecture-level only (Proxmox, Synology, Docker, VLAN concept). No concrete IPs, WireGuard keys, passwords, internal hostnames.
---
## 🌍 Language
## Language
All five sites are written **entirely in English**. German source documents are translated once during migration (AI-assisted). New content is authored in English from the start.
All moonweb sites are written **entirely in English**. The timecapsule uses the original 2001 English content.
---
## 📚 Documentation
## Documentation
| File | Purpose |
|------|---------|
| `DESIGN.md` | Initial concept and design decisions (German) |
| `SPEC.md` | Complete specification what gets built (English) |
| `DESIGN.md` | Initial concept and design decisions |
| `SPEC.md` | Complete specification - what gets built |
| `PLAN.md` | Phased implementation plan |
| `TODO.md` | Open items, workflow, and current status |
| `README.md` | This file project overview for GitHub |
| `README.md` | This file - project overview for GitHub |
---
## 📄 License
## License
[![CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
+71 -60
View File
@@ -1,76 +1,87 @@
# TODO — moonweb-site Offene Items
# TODO: Consolidation zu www.moonweb.org
Stand: 2026-08-14
## Uebersicht
## Aktueller Status
Alle moonweb.org Sites (hub, infra, smarthome, code, retro, timecapsule) unter `www.moonweb.org` als Subverzeichnisse vereinen. Deployment von Cloudflare Pages zu IONOS SFTP migrieren.
| Phase | Status | Bemerkung |
|-------|--------|-----------|
| Phase 0 — Repo & Tooling | ✅ fertig | Eleventy 3.1.6, CI/CD, .gitignore erweitert |
| Phase 1 — Content | ✅ fertig | hub, code, infra, smarthome komplett, retro bewusst rudimentär |
| Phase 2 — CI/CD | ✅ fertig | build-deploy.yml mit Caching + Validation + Summary |
| Phase 3 — Launch | ✅ fertig | Cloudflare Pages live, Smoke-Test bestanden |
| Phase 4 — Deferred | ⏸️ zurückgestellt | 28k8, Analytics |
| Phase 5 — stefankoelle.de | ✅ fertig | Ins Monorepo migriert, Eleventy-Build, SFTP-Deployment |
**URL-Struktur nach Migration:**
| URL | Inhalt |
|-----|--------|
| `www.moonweb.org/` | Hub (neue Root) |
| `www.moonweb.org/infra/` | Infra |
| `www.moonweb.org/smarthome/` | Smarthome |
| `www.moonweb.org/code/` | Code |
| `www.moonweb.org/retro/` | Retro |
| `www.moonweb.org/timecapsule/` | Altes www (2001-Retro) |
| `stefankoelle.de/` | CV (bleibt separat) |
## Offene Items
---
### P4 — Deferred (nicht dringend)
## Phase 1: Timecapsule in Monorepo integrieren
- [ ] 28k8.moonweb.org Zukunft klären
- [ ] Perplexity-Rückkanal
- [ ] Apex-Domain Redirect (moonweb.org → hub.moonweb.org)
- [ ] PDF-Build fuer CV (WeasyPrint, lokal testen)
- [ ] Querverlinkungen stefankoelle.de <-> smarthome
- [ ] Ledmatrix ggf. nach smarthome verschieben
- [ ] **Dotnet Home-Automation-Projekt beschreiben** — Interne .NET Core Software für Kommunikation auf buttons.html und InfluxDB. Code muss erst aufgeraeumt und nach GitHub publiziert werden, dann Beschreibung auf stefankoelle.de und ggf. smarthome ergänzen.
- [x] 1.1 Dateien aus ../moonweb-www/src/ nach timecapsule/src/ kopieren
- [x] 1.2 timecapsule/eleventy.config.js erstellen (Eleventy 2.x)
- [x] 1.3 timecapsule/package.json erstellen (eigene Dependencies)
- [x] 1.4 timecapsule/src/_data/site.json anpassen (Domain mit /timecapsule/)
- [x] 1.5 Alle internen Pfade mit /timecapsule/ prefixieren
- [x] 1.6 Passthrough Copy in eleventy.config.js anpassen
- [x] 1.7 Build-Output pruefen (dist/timecapsule/)
### Offene Entscheidungen
## Phase 2: Eleventy-Configs aller Sites anpassen
- [ ] **stefankoelle.de SFTP: `delete_remote_files`?** — Derzeit werden beim Deploy nur Dateien hochgeloaden, alte bleiben liegen. Mit `delete_remote_files: true` waere jeder Deploy ein sauberes Image (alles weg + neu). Kurzer Ausfall moeglich. Entscheidung offen.
- [x] 2.1 hub/eleventy.config.js: site.url auf www.moonweb.org
- [x] 2.2 infra/eleventy.config.js: site.url auf www.moonweb.org
- [x] 2.3 smarthome/eleventy.config.js: site.url auf www.moonweb.org
- [x] 2.4 code/eleventy.config.js: site.url auf www.moonweb.org
- [x] 2.5 retro/eleventy.config.js: site.url auf www.moonweb.org
- [x] 2.6 shared/_includes/base.njk anpassen
- [x] 2.7 hub/index.njk: Card-Hrefs relativieren
- [x] 2.8 hub/impressum.njk: Domain-Liste aktualisieren
## Schon erledigt (Stand 2026-08-14)
## Phase 3: Build-System anpassen
### stefankoelle.de Migration (2026-08-14)
- [x] Eleventy-Config + package.json erweitert
- [x] Onepager-Layout mit Anchor-Links beibehalten
- [x] CV-Partial (zentral fuer Web + PDF)
- [x] CSS ausgelagert + modernisiert (Custom Properties, box-sizing)
- [x] Assets in assets/ verschoben
- [x] Ledmatrix als eigene Seite integriert
- [x] Alte Dateien geloescht (index.html, browserconfig.xml, sitemap.txt)
- [x] Deployment-Workflow fuer IONOS SFTP
- [x] Cloudflare-Workflow um stefankoelle erweitert (paths-ignore)
- [x] 3.1 package.json: Neue Scripts fuer timecapsule + merge
- [x] 3.2 scripts/merge-moonweb.sh erstellen
- [x] 3.3 Lokal Build testen (alle Sites)
### Vorherige Erledigungen (Stand 2026-08-13)
- [x] Impressum angelegt + Footer-Link auf allen Sites
- [x] Favicons (SVG mit Emojis) pro Subdomain — `shared/favicon/*.svg`, per Passthrough Copy
- [x] Meta descriptions auf allen 33 Seiten
- [x] CSS @import-Kette bereinigt (kein Render-Blocking mehr)
- [x] .gitignore erweitert (.env*, Zertifikate, Swap-Files)
- [x] Placeholder-Cards `"#"` bereinigt — keine mehr vorhanden
- [x] README.md überarbeitet (Emojis, Architektur, Tech-Stack, CI/CD)
- [x] Lizenz hinzugefügt (CC BY-NC-SA 4.0)
- [x] Content-Review — alle Seiten geprüft, alles passt
- [x] Cloudflare Pages Projects erstellt (moonweb-hub, -infra, -smarthome, -code, -retro)
- [x] DNS & Custom Domains gebunden
- [x] Secrets konfiguriert (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID)
- [x] Smoke-Test Cross-Linking — alles funktioniert auf live
## Phase 4: Deployment umstellen
## Workflow für neue Contexts
- [x] 4.1 Neuen Workflow .github/workflows/build-deploy-moonweb.yml erstellen
- [x] 4.2 Alten Workflow .github/workflows/build-deploy.yml entfernen
- [ ] 4.3 Alten deploy-moonweb.yml in moonweb-www deaktivieren
Wenn du einen neuen Context öffnest, lies diese Datei und prüfe:
1. Welche P4-Items sind relevant? → Nur wenn angefordert
## Phase 5: Cloudflare Redirects
## Definition of done
- [x] 5.1 Redirect-Regeln einrichten (via GitHub Action)
Alle 5 Sites live auf Cloudflare Pages:
- ✅ hub.moonweb.org — verlinkt alles (+ stefankoelle.de extern)
- ✅ code.moonweb.org — GitHub-Katalog via Aggregator
- ✅ smarthome.moonweb.org — Smart Home Übersicht + Detailseiten
- ✅ infra.moonweb.org — Infra-Übersicht (redigiert)
- ✅ retro.moonweb.org — Ehrliche minimale Übersicht (WIP)
| Quell-Domain | Ziel-URL | Type |
|-------------|----------|------|
| `hub.moonweb.org/*` | `https://www.moonweb.org/$1` | 301 |
| `infra.moonweb.org/*` | `https://www.moonweb.org/infra/$1` | 301 |
| `smarthome.moonweb.org/*` | `https://www.moonweb.org/smarthome/$1` | 301 |
| `code.moonweb.org/*` | `https://www.moonweb.org/code/$1` | 301 |
| `retro.moonweb.org/*` | `https://www.moonweb.org/retro/$1` | 301 |
stefankoelle.de live auf IONOS (SFTP-Deployment):
- ✅ stefankoelle.de — Onepager mit CV, Projects, Languages, Impressum
- ✅ stefankoelle.de/ledmatrix/ — LED Matrix WebServer Dokumentation
**Umsetzung:** `scripts/cloudflare/setup-redirects.sh` + GitHub Action `cloudflare-redirects.yml`
**Benötigte Secrets:** `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ZONE_ID`
## Phase 6: SEO
- [ ] 6.1 Google Search Console: Neue Property www.moonweb.org
- [ ] 6.2 Sitemap submiten
## Phase 7: Cleanup
- [ ] 7.1 moonweb-www Repository archivieren
- [ ] 7.2 Cloudflare Pages Projects loeschen (nach Redirect-Test)
---
## Abgeschlossen
Alle Code-Aenderungen sind fertig. Nächste Schritte:
1. Commit auf feature/consolidate-www Branch
2. Push und PR erstellen
3. Deployen
4. Cloudflare Redirects einrichten
5. Google Search Console aktualisieren
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Page Not Found"
section: "code"
tags: "code"
description: "The page you are looking for does not exist."
layout: base.njk
permalink: /404.html
permalink: /code/404.html
---
<div class="error-page">
<h1>404</h1>
-20
View File
@@ -1,20 +0,0 @@
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData("site", { url: "https://code.moonweb.org" });
eleventyConfig.addFilter("date", (d) => d.toISOString());
eleventyConfig.addPassthroughCopy({ "shared/theme-code.css": "theme.css" });
eleventyConfig.addPassthroughCopy({ "shared/base.css": "shared-base.css" });
eleventyConfig.addPassthroughCopy({ "shared/favicon/code.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({ "code/robots.txt": "robots.txt" });
return {
dir: {
input: "code",
includes: "../shared/_includes",
output: "dist/code"
},
serverOptions: {
host: "0.0.0.0",
port: 8084
}
};
};
+1
View File
@@ -1,6 +1,7 @@
---
title: "Open Source Projects — GitHub Repos & Code | moonweb"
section: "code"
tags: "code"
description: "Curated catalog of my public GitHub projects — automation, dev tools, web apps, and firmware."
layout: base.njk
subcategories:
-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://code.moonweb.org/sitemap.xml
-5
View File
@@ -1,5 +0,0 @@
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
{% include "sitemap.njk" %}
+91
View File
@@ -0,0 +1,91 @@
const SITES = {
hub: { pathPrefix: "" },
infra: { pathPrefix: "/infra" },
smarthome: { pathPrefix: "/smarthome" },
code: { pathPrefix: "/code" },
retro: { pathPrefix: "/retro" },
};
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData("site", { url: "https://www.moonweb.org" });
eleventyConfig.addFilter("date", (d) => d.toISOString());
// Computed pathPrefix based on page's section front matter
eleventyConfig.addGlobalData("eleventyComputed", {
pathPrefix: (data) => SITES[data.section]?.pathPrefix ?? "",
});
// Per-section collections (for sitemaps)
for (const section of Object.keys(SITES)) {
eleventyConfig.addCollection(section, (collectionsApi) =>
collectionsApi.getFilteredByTag(section)
);
}
// --- Passthrough copies ---
// Root-level shared assets
eleventyConfig.addPassthroughCopy({ "shared/base.css": "shared-base.css" });
eleventyConfig.addPassthroughCopy({ "shared/favicon/hub.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({ "hub/robots.txt": "robots.txt" });
// Per-section assets: favicon
for (const [section, cfg] of Object.entries(SITES)) {
const prefix = cfg.pathPrefix || ".";
eleventyConfig.addPassthroughCopy({
[`shared/favicon/${section}.svg`]: `${prefix}/favicon.svg`,
});
}
// Hub: profile photo
eleventyConfig.addPassthroughCopy({
"hub/stefan-koelle-foto.jpg": "stefan-koelle-foto.jpg",
});
// Hub: old www.moonweb.org redirects (.htm → .html)
const htmRedirects = [
"start",
"index2",
"index3",
"new",
"links",
"infos",
"hardware",
"users",
];
for (const name of htmRedirects) {
eleventyConfig.addPassthroughCopy({
[`hub/${name}.htm`]: `${name}.html`,
});
}
const htmSubdirs = [
"beginning",
"projects",
"products",
"acoustics",
"location",
"background",
"partners",
"contact",
"network",
"stefan",
"legal-notice",
];
for (const dir of htmSubdirs) {
eleventyConfig.addPassthroughCopy({
[`hub/${dir}/index.htm`]: `${dir}/index.html`,
});
}
return {
dir: {
input: ".",
includes: "shared/_includes",
output: "dist",
},
serverOptions: {
host: "0.0.0.0",
port: 8081,
},
};
};
+1
View File
@@ -1,6 +1,7 @@
---
title: "Page Not Found"
section: "hub"
tags: "hub"
description: "The page you are looking for does not exist."
layout: base.njk
permalink: /404.html
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/acoustics/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/acoustics/">/timecapsule/acoustics/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/background/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/background/">/timecapsule/background/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/beginning/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/beginning/">/timecapsule/beginning/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/contact/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/contact/">/timecapsule/contact/</a>...</p>
</body>
</html>
-21
View File
@@ -1,21 +0,0 @@
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData("site", { url: "https://hub.moonweb.org" });
eleventyConfig.addFilter("date", (d) => d.toISOString());
eleventyConfig.addPassthroughCopy({ "shared/theme-hub.css": "theme.css" });
eleventyConfig.addPassthroughCopy({ "shared/base.css": "shared-base.css" });
eleventyConfig.addPassthroughCopy({ "shared/favicon/hub.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({ "hub/robots.txt": "robots.txt" });
eleventyConfig.addPassthroughCopy({ "hub/stefan-koelle-foto.jpg": "stefan-koelle-foto.jpg" });
return {
dir: {
input: "hub",
includes: "../shared/_includes",
output: "dist/hub"
},
serverOptions: {
host: "0.0.0.0",
port: 8081
}
};
};
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+16 -9
View File
@@ -1,6 +1,8 @@
---
title: "Legal Notice & Privacy Policy"
section: "hub"
tags: "hub"
permalink: /impressum/index.html
description: "Legal notice (Impressum) and privacy policy for moonweb.org."
layout: base.njk
---
@@ -19,7 +21,7 @@ layout: base.njk
</p>
<p>
Scope of this legal notice: This legal notice and the privacy policy below apply to the following websites, all operated by Stefan Kölle: hub.moonweb.org, smarthome.moonweb.org, infra.moonweb.org, code.moonweb.org, retro.moonweb.org, buildbroken.moonweb.org, 28k8.moonweb.org, www.moonweb.org, and stefankoelle.de. All of these sites are part of the same personal project network and are covered jointly by this single document.
Scope of this legal notice: This legal notice and the privacy policy below apply to the following websites, all operated by Stefan Kölle: www.moonweb.org (including /infra/, /smarthome/, /code/, /retro/, /timecapsule/), buildbroken.moonweb.org, 28k8.moonweb.org, and stefankoelle.de. All of these sites are part of the same personal project network and are covered jointly by this single document.
</p>
<p>
@@ -41,7 +43,7 @@ layout: base.njk
<h3>2. Purpose of these Websites</h3>
<p>
The moonweb.org subdomains (hub, smarthome, infra, code, retro) document my private home-lab, home-automation and retro-computing hobby projects. buildbroken.moonweb.org is an archive of a blog I created with friends in our spare time. 28k8.moonweb.org is a homage to the 90s era. www.moonweb.org is an archived version of my original website from 2001. stefankoelle.de presents my professional CV and background as the same person. All are part of one personal project network and share this legal notice and privacy policy.
The moonweb.org websites (hub, smarthome, infra, code, retro, timecapsule) document my private home-lab, home-automation and retro-computing hobby projects. buildbroken.moonweb.org is an archive of a blog I created with friends in our spare time. 28k8.moonweb.org is a homage to the 90s era. stefankoelle.de presents my professional CV and background as the same person. All are part of one personal project network and share this legal notice and privacy policy.
</p>
<h3>3. Hosting</h3>
@@ -49,18 +51,23 @@ layout: base.njk
Two different hosting providers are used within this network:
</p>
<h4>a) moonweb.org subdomains (hub, smarthome, infra, code, retro, buildbroken) — Cloudflare Pages</h4>
<p>
Hosted via Cloudflare Pages, operated by Cloudflare, Inc., 101 Townsend St, San Francisco, CA 94107, USA. Cloudflare processes connection-level data (e.g. your IP address, request headers) as part of its content delivery and DDoS/security systems. This is governed by Cloudflare's Data Processing Addendum, which incorporates the EU Standard Contractual Clauses (2021) as a safeguard for the transfer of data to the USA. See <a href="https://www.cloudflare.com/privacypolicy/">cloudflare.com/privacypolicy</a>.
</p>
<h4>b) www.moonweb.org and stefankoelle.de — IONOS SE</h4>
<h4>a) www.moonweb.org (hub, infra, smarthome, code, retro, timecapsule) &mdash; IONOS SE</h4>
<p>
Hosted on web space provided by IONOS SE, Elgendorfer Str. 57, 56410 Montabaur, Germany, acting as a processor under Art. 28 GDPR (Data Processing Agreement in place).
</p>
<h4>b) stefankoelle.de &mdash; IONOS SE</h4>
<p>
Legal basis for both: Art. 6(1)(f) GDPR (legitimate interest in reliable, secure delivery of the websites).
Hosted on web space provided by IONOS SE, Elgendorfer Str. 57, 56410 Montabaur, Germany, acting as a processor under Art. 28 GDPR (Data Processing Agreement in place).
</p>
<h4>c) buildbroken.moonweb.org, 28k8.moonweb.org &mdash; Cloudflare Pages</h4>
<p>
Hosted via Cloudflare Pages, operated by Cloudflare, Inc., 101 Townsend St, San Francisco, CA 94107, USA. Cloudflare processes connection-level data (e.g. your IP address, request headers) as part of its content delivery and DDoS/security systems. This is governed by Cloudflare&rsquo;s Data Processing Addendum, which incorporates the EU Standard Contractual Clauses (2021) as a safeguard for the transfer of data to the USA. See <a href="https://www.cloudflare.com/privacypolicy/">cloudflare.com/privacypolicy</a>.
</p>
<p>
Legal basis: Art. 6(1)(f) GDPR (legitimate interest in reliable, secure delivery of the websites).
</p>
<h3>4. No Tracking, No Cookies, No Analytics (current state)</h3>
+24 -5
View File
@@ -1,6 +1,8 @@
---
title: "Stefan Koelle — Homelab, Smart Home, Code & Retro Hardware | moonweb"
section: "hub"
tags: "hub"
permalink: /index.html
description: "Stefan Koelle — Senior Software Developer & Architect based in Munich. Homelab projects, smart home automation, open-source code, and retro hardware."
layout: base.njk
ogImage: "/stefan-koelle-foto.jpg"
@@ -9,19 +11,19 @@ sections:
cards:
- title: "infra"
summary: "Stack overview: Proxmox, Synology, network, backups, and how I work."
href: "https://infra.moonweb.org"
href: "/infra/"
emoji: "🏗️"
- title: "smarthome"
summary: "What's actually running on the homelab, and why."
href: "https://smarthome.moonweb.org"
href: "/smarthome/"
emoji: "🏠"
- title: "code"
summary: "Curated catalog of my GitHub projects."
href: "https://code.moonweb.org"
href: "/code/"
emoji: "💻"
- title: "retro"
summary: "Physical retro hardware collection."
href: "https://retro.moonweb.org"
href: "/retro/"
emoji: "🕹️"
- heading: "About me"
cards:
@@ -37,7 +39,7 @@ sections:
emoji: "💾"
- title: "www.moonweb.org"
summary: "The 2000s internet projects, networking and engineering."
href: "https://www.moonweb.org"
href: "/timecapsule/"
emoji: "🌐"
- title: "inseco.de"
summary: "Internet services & consulting for gastronomy and clubs, Peak 20032008. Custom PHP/CMS."
@@ -59,3 +61,20 @@ sections:
</div>
{% include "card-grid.njk" %}
<section class="about-expanded">
<h2>What this site is about</h2>
<p>moonweb is a collection of personal projects that have grown over the past 25 years. What started as a single 486 running a BBS in the mid-90s has turned into a full homelab with Proxmox, Docker, smart home automation, and a growing collection of retro hardware. The name "moonweb" comes from the original domain moonweb.org, which has been online since 2000.</p>
<p>Each section of this site documents a different part of the setup. The <a href="/infra/">infrastructure section</a> covers the Proxmox cluster, Synology NAS, Docker containers, networking, and backup strategies. The <a href="/smarthome/">smart home section</a> shows what automation projects are running, from Tasmota energy monitoring to MQTT sensors and AirPlay audio. The <a href="/code/">code section</a> lists the public GitHub projects that power these setups, and the <a href="/retro/">retro section</a> documents the hardware collection.</p>
<h2>What I do professionally</h2>
<p>I work as a Senior Software Developer &amp; Architect at TENHIL GmbH (formerly stellenanzeigen.de), where I build and maintain microservices on .NET Core and Kubernetes. My day job involves designing cloud-native architectures, building APIs, and migrating legacy applications to modern stacks. I've been working with .NET since the early 2000s and have used everything from ASP Classic to the latest .NET 8 features.</p>
<p>Since early 2025, I've fully embraced AI-assisted development. I use Claude Code for most of my private projects and at work for Jira story implementation. The productivity gains are massive, and it's changed how I approach software development entirely. I also use OpenCode with OpenRouter for Python-based home lab projects.</p>
<h2>The homelab</h2>
<p>The homelab has been evolving since the mid-90s. Today it runs on a Proxmox cluster with a Synology DS918+ NAS, multiple Docker hosts, and an OpenWrt router for multi-WAN failover. The setup includes Prometheus and Grafana for monitoring, InfluxDB for time-series data from smart home sensors, and a full backup strategy covering NAS snapshots, offsite copies, and cloud storage.</p>
<p>The smart home side includes HomematicIP thermostats bridged to MQTT, Tasmota smart plugs for energy monitoring, AirPlay multi-room audio on Raspberry Pi devices, and various ESP32-based dashboards and LED matrix displays. Everything is documented in the respective sections of this site.</p>
<h2>Retro computing</h2>
<p>Outside of work, I collect and maintain retro hardware. The collection includes Atari ST machines (a Mega ST 4 fleet and a Mega ST 2), Amiga systems, a 486 DX2-66 tower, and various MiSTer and MiST FPGA setups for preservation. I also run Batocera in the living room for casual retro gaming. The retro section documents the hardware, the maintenance, and the occasional deep-dive into compatibility issues like the 15kHz monitor problem on the Atari ST.</p>
</section>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/legal-notice/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/legal-notice/">/timecapsule/legal-notice/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/location/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/location/">/timecapsule/location/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/network/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/network/">/timecapsule/network/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/partners/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/partners/">/timecapsule/partners/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/products/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/products/">/timecapsule/products/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/projects/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/projects/">/timecapsule/projects/</a>...</p>
</body>
</html>
+2 -1
View File
@@ -1,4 +1,5 @@
User-agent: *
Allow: /
Sitemap: https://hub.moonweb.org/sitemap.xml
Sitemap: https://www.moonweb.org/sitemap.xml
Sitemap: https://www.moonweb.org/timecapsule/sitemap.xml
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/stefan/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/stefan/">/timecapsule/stefan/</a>...</p>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/timecapsule/">
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting to <a href="/timecapsule/">/timecapsule/</a>...</p>
</body>
</html>
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Page Not Found"
section: "infra"
tags: "infra"
description: "The page you are looking for does not exist."
layout: base.njk
permalink: /404.html
permalink: /infra/404.html
---
<div class="error-page">
<h1>404</h1>
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "Backup Strategy"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "Layered 3-2-1 backup strategy covering NAS snapshots, offsite copies, and cloud storage."
layout: base.njk
---
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "Xubuntu Dev VM"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "A disposable, daily-backed-up Xubuntu VM for AI-assisted, remote development via SSH and Chrome Remote Desktop."
layout: base.njk
---
+5 -3
View File
@@ -1,7 +1,8 @@
---
title: "Docker Container Strategy"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "How self-hosted services are organized across three Docker hosts on Proxmox and Synology, using Docker Compose as the single source of truth."
layout: base.njk
---
@@ -53,11 +54,12 @@ layout: base.njk
<h2>Application containers</h2>
<ul>
<li><strong>SearXNG</strong> — self-hosted, privacy-respecting metasearch engine.</li>
<li><strong>SearXNG</strong> — self-hosted, privacy-respecting metasearch engine that aggregates results from multiple search providers without leaking queries to any of them. Used as the default search backend for local browsing and AI tools.</li>
<li><strong>Open WebUI</strong> — chat interface for locally hosted LLMs.</li>
<li><strong>Stirling PDF</strong> — self-hosted PDF toolkit for merging, converting and editing documents.</li>
<li><strong>Stirling PDF</strong> — self-hosted PDF toolkit for merging, splitting, converting, compressing, and editing documents without uploading them to external services.</li>
<li><strong>ArchiveBox</strong> — personal web archiving of bookmarked pages.</li>
<li><strong>TubeSync / TubeArchivist [NAS]</strong> — download and archive YouTube content directly onto NAS storage; these run on docker-host-nas specifically because they need direct volume access to the Synology's disks.</li>
<li><strong>Music Stretto</strong> — music streaming server for the local MP3 library on the Synology, accessible from the home dashboard.</li>
</ul>
<h2>Development tools</h2>
-20
View File
@@ -1,20 +0,0 @@
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData("site", { url: "https://infra.moonweb.org" });
eleventyConfig.addFilter("date", (d) => d.toISOString());
eleventyConfig.addPassthroughCopy({ "shared/theme-infra.css": "theme.css" });
eleventyConfig.addPassthroughCopy({ "shared/base.css": "shared-base.css" });
eleventyConfig.addPassthroughCopy({ "shared/favicon/infra.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({ "infra/robots.txt": "robots.txt" });
return {
dir: {
input: "infra",
includes: "../shared/_includes",
output: "dist/infra"
},
serverOptions: {
host: "0.0.0.0",
port: 8082
}
};
};
+13
View File
@@ -1,6 +1,7 @@
---
title: "Homelab Infrastructure — Proxmox, Docker & Networking | moonweb"
section: "infra"
tags: "infra"
description: "Homelab infrastructure overview: Proxmox hosts, Synology NAS, Docker VMs and Setup-Guides."
layout: base.njk
sections:
@@ -36,3 +37,15 @@ sections:
<p class="site-intro">A peek under the hood of my homelab. Proxmox, Synology, Docker, and how everything is wired together.</p>
{% include "card-grid.njk" %}
<section class="about-expanded">
<h2>The setup at a glance</h2>
<p>The infrastructure runs on three main machines: a Proxmox VE host (Intel NUC, 64GB RAM) as the primary compute node, a Synology DS918+ NAS for storage and Docker services, and a Xubuntu VM on Proxmox for development. An OpenWrt router handles multi-WAN failover with WiFi and Powerline backhaul, and four Zyxel GS1200-8HP managed switches distribute the network across the flat.</p>
<p>The whole stack is monitored via Prometheus and Grafana, with node exporters on every machine, cAdvisor for container metrics, and SNMP exporters for network hardware. Backups follow a 3-2-1 strategy: local snapshots, NAS copies, offsite sync to a Raspberry Pi in a different room, and cloud archive for worst-case scenarios.</p>
<h2>Why Proxmox</h2>
<p>Proxmox VE was chosen over alternatives like ESXi or plain Docker for several reasons. The open-source model means no licensing headaches. LXC containers provide near-native performance for lightweight services without the overhead of full VMs. The web UI makes it easy to manage snapshots, backups, and migrations. And the ZFS support on the underlying Debian system gives us reliable snapshots and send/receive for the backup chain.</p>
<h2>How services are organized</h2>
<p>The general rule: stateful services (databases, media archives, monitoring data) run on the Synology NAS where they benefit from the integrated backup and snapshot system. Stateless services and compute-heavy workloads run on the Proxmox Docker VM. LXC containers handle lightweight always-on services like Pi-hole and MariaDB. This separation makes it easy to tear down and recreate Docker containers without worrying about data loss.</p>
</section>
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "LXC Container Strategy"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "Overview of the lightweight LXC containers running on the main Proxmox host, and why they are LXC instead of Docker."
layout: base.njk
---
+22 -2
View File
@@ -1,7 +1,8 @@
---
title: "Monitoring"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "Prometheus and Grafana monitoring stack for hosts, containers, and hardware metrics."
layout: base.njk
---
@@ -9,7 +10,9 @@ layout: base.njk
<div class="detail-content">
<p>Prometheus and Grafana form the central monitoring stack for the whole
homelab, running as Docker containers on the NAS.</p>
homelab, running as Docker containers on the NAS. Every machine in the
flat — Proxmox host, Docker VM, Synology NAS, and Raspberry Pi devices —
feeds metrics into this central instance.</p>
<h2>What's monitored</h2>
<ul>
@@ -27,6 +30,23 @@ homelab, running as Docker containers on the NAS.</p>
<tr><td>Network printer</td><td>Toner level, page count, online/offline status via SNMP</td></tr>
</table>
<h2>How it's deployed</h2>
<p>Prometheus runs as a Docker container on the Synology NAS with a
bind-mounted data directory for persistence. Grafana is deployed as a
sibling container, connected to Prometheus as its primary datasource.
Both containers are defined in a single Docker Compose file and managed
via Portainer. The Prometheus configuration file defines scrape targets,
scrape intervals (15 seconds for hosts, 60 seconds for network devices),
and retention policies.</p>
<h2>Alerting</h2>
<p>Basic alerting is configured through Prometheus Alertmanager, which
sends notifications to a dedicated Telegram channel for critical events
like host-down or disk-full conditions. The dead-man's-switch pattern
ensures that if Prometheus itself stops scraping, an alert fires within
minutes. More sophisticated alerting rules (disk space prediction,
temperature thresholds) are planned but not yet implemented.</p>
<h2>Open items</h2>
<ul>
<li>Alerting rules are not yet configured for most services.</li>
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "OpenWrt Multi-WAN Router"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "Multi-WAN OpenWrt router in the study with WiFi and Powerline failover, plus VLAN 189 for IoT isolation."
layout: base.njk
---
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "Proxmox Virtualization"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "What Proxmox VE is, the hardware it runs on in my homelab, its key advantages, and a short setup walkthrough."
layout: base.njk
---
-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://infra.moonweb.org/sitemap.xml
-5
View File
@@ -1,5 +0,0 @@
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
{% include "sitemap.njk" %}
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "Synology NAS"
section: "infra"
parent: "/"
tags: "infra"
parent: "/infra/"
description: "Model, storage setup and current role of the Synology NAS in the homelab after the late-2025 cleanup."
layout: base.njk
---
+7 -14
View File
@@ -1,23 +1,16 @@
{
"name": "moonweb-site",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"description": "Monorepo for hub/infra/smarthome/code/retro (Eleventy, static, deployed to Cloudflare Pages)",
"description": "Monorepo for hub/infra/smarthome/code/retro/timecapsule (Eleventy, static, deployed to IONOS SFTP)",
"scripts": {
"dev:hub": "npx @11ty/eleventy --config=hub/eleventy.config.js --serve --port=8081",
"dev:infra": "npx @11ty/eleventy --config=infra/eleventy.config.js --serve --port=8082",
"dev:smarthome": "npx @11ty/eleventy --config=smarthome/eleventy.config.js --serve --port=8083",
"dev:code": "npx @11ty/eleventy --config=code/eleventy.config.js --serve --port=8084",
"dev:retro": "npx @11ty/eleventy --config=retro/eleventy.config.js --serve --port=8085",
"dev": "npx @11ty/eleventy --serve --port=8081",
"dev:stefankoelle": "npx @11ty/eleventy --config=stefankoelle/eleventy.config.js --serve --port=8086",
"dev": "npm run dev:hub & npm run dev:infra & npm run dev:smarthome & npm run dev:code & npm run dev:retro & npm run dev:stefankoelle",
"build:hub": "npx @11ty/eleventy --config=hub/eleventy.config.js",
"build:infra": "npx @11ty/eleventy --config=infra/eleventy.config.js",
"build:smarthome": "npx @11ty/eleventy --config=smarthome/eleventy.config.js",
"build:code": "npx @11ty/eleventy --config=code/eleventy.config.js",
"build:retro": "npx @11ty/eleventy --config=retro/eleventy.config.js",
"dev:timecapsule": "cd timecapsule && npx @11ty/eleventy --serve --port=8087",
"build": "npx @11ty/eleventy",
"build:stefankoelle": "npx @11ty/eleventy --config=stefankoelle/eleventy.config.js",
"build": "npm run build:hub && npm run build:infra && npm run build:smarthome && npm run build:code && npm run build:retro && npm run build:stefankoelle",
"build:timecapsule": "cd timecapsule && npx @11ty/eleventy && mkdir -p ../dist/timecapsule && cp -r _site/timecapsule/* ../dist/timecapsule/",
"build:moonweb": "npm run build && npm run build:timecapsule",
"pdf:cv": "bash stefankoelle/pdf/build-pdf.sh",
"prebuild": "bash prebuild.sh"
},
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Page Not Found"
section: "retro"
tags: "retro"
description: "The page you are looking for does not exist."
layout: base.njk
permalink: /404.html
permalink: /retro/404.html
---
<div class="error-page">
<h1>404</h1>
+27 -2
View File
@@ -1,14 +1,15 @@
---
title: "Running AmigaOS in 2026: MiSTer FPGA vs Amiberry on Pi"
section: "retro"
tags: "retro"
description: "Two very different ways to get a fully working AmigaOS 3.2.3 desktop running today."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>Running AmigaOS in 2026: MiSTer FPGA vs Amiberry on Pi</h1>
<div class="detail-content">
<p>AmigaOS 3.2.3 runs beautifully both on FPGA and on modern ARM boards — the question is which trade-offs you're willing to accept.</p>
<p>AmigaOS 3.2.3 runs beautifully both on FPGA and on modern ARM boards — the question is which trade-offs you're willing to accept. Both approaches have their place, and the choice often comes down to what you're trying to do: run demos at perfect timing, or tinker with software at higher resolutions.</p>
<h2>Two Roads to the Same Workbench</h2>
<ul>
@@ -18,5 +19,29 @@ parent: "/"
<li><strong>Picking a monitor</strong> — a 4:3 panel at native 1280×1024 (like an older NEC MultiSync) gives the most period-accurate Workbench experience; anything widescreen needs scaling compromises.</li>
</ul>
<h2>When to choose MiSTer</h2>
<p>The MiSTer shines when timing accuracy matters. Demos that rely on
cycle-exact raster effects, games that use custom hardware tricks, and
software that depends on precise interrupt timing all benefit from the
FPGA's hardware-level reproduction. The Minimig-AGA core also provides
a more authentic floppy drive experience, including the characteristic
drive sounds and seek times that software sometimes depends on for
loading screens and copy protection.</p>
<h2>When to choose Amiberry on Pi</h2>
<p>The Pi is better for productivity and development work. Higher RTG
resolutions make Workbench usable on modern monitors. The ability to
easily swap between AmigaOS versions and hard disk images makes it
ideal for testing software. And the lower cost means you can dedicate
a Pi to Amiga development without tying up expensive FPGA hardware.</p>
<h2>The compatibility question</h2>
<p>Most Amiga software works on both platforms, but there are edge cases.
Some demos use raster tricks that only work on real AGA hardware or the
MiSTer's FPGA core. Some productivity software depends on specific
accelerator card features that neither platform emulates perfectly.
And the perennial question of Kickstart ROM versions — 3.1 for
compatibility, 3.2 for features — applies equally to both setups.</p>
<p class="redacted-note">Both setups now run side by side without any real winner — MiSTer for authenticity and demos, the Pi for tinkering and RTG resolutions.</p>
</div>
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Vampire V4, A600GS or MiSTer: Amiga Hardware Isn't a Simple Speed Race"
section: "retro"
tags: "retro"
description: "Why raw benchmark numbers are actively misleading when comparing modern Amiga hardware options."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>Vampire V4, A600GS or MiSTer: Amiga Hardware Isn't a Simple Speed Race</h1>
<div class="detail-content">
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "The Atari Mega ST Fleet: Three Machines, Three Jobs"
section: "retro"
tags: "retro"
description: "How three Atari Mega ST computers ended up with completely different roles in a modern retro setup."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>The Atari Mega ST Fleet: Three Machines, Three Jobs</h1>
<div class="detail-content">
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "The 15kHz Problem: Finding a Monitor That Speaks Atari"
section: "retro"
tags: "retro"
description: "Why most modern monitors refuse to display Atari ST colour modes, and which ones actually work."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>The 15kHz Problem: Finding a Monitor That Speaks Atari</h1>
<div class="detail-content">
+31 -1
View File
@@ -1,9 +1,10 @@
---
title: "Getting a 40-Year-Old Computer Online: Atari ST Networking and BBS Access"
section: "retro"
tags: "retro"
description: "Connecting an Atari ST to the modern internet and to dial-up-era bulletin board systems, warts and all."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>Getting a 40-Year-Old Computer Online: Atari ST Networking and BBS Access</h1>
<div class="detail-content">
@@ -18,5 +19,34 @@ parent: "/"
<li><strong>File transfer the easy way</strong> — a plain FTP client on the Atari talking to a NAS on the local network sidesteps most of the internet-connectivity headaches entirely, especially for moving disk images and software archives back and forth.</li>
</ul>
<h2>Why connect a 40-year-old computer to the internet</h2>
<p>The obvious question is "why bother?" — the Atari ST was never designed
for the internet, and the experience of using it online is objectively
worse than any modern device. But that's not the point. The real
satisfaction comes from making old hardware do things it was never
designed to do, understanding the networking stack at a level that modern
abstractions hide, and experiencing the dial-up era's BBS culture on
original hardware.</p>
<h2>The BBS experience</h2>
<p>Bulletin board systems from the dial-up era are still running today,
maintained by enthusiasts who keep the old software alive. Connecting to
one from an original Atari ST — via a Wi-Fi modem emulation — is a
genuinely nostalgic experience. The ANSI art login screens, the message
boards, the file sections, the door games — it's all exactly as it was
in the early 1990s. The main difference: instead of tying up a phone
line, the connection goes over Wi-Fi to a BBS that's accessible from
anywhere in the world.</p>
<h2>Lessons learned</h2>
<p>The biggest lesson: networking on retro hardware is 90% patience and
10% actual configuration. The STinG stack is well-documented but
fragile — wrong TCP/IP settings silently break connections without
useful error messages. The ESP32 Wi-Fi modules are cheap and flexible
but require careful firmware selection for the specific use case. And
terminal emulation is a rabbit hole of character sets, escape sequences,
and display quirks that haven't been relevant for decades but still
matter when connecting to a BBS that renders ANSI art.</p>
<p class="redacted-note">If you're chasing BBS nostalgia on real hardware: budget far more patience for the networking layer than for the actual computer. The 1980s hardware is the easy part; convincing 2020s Wi-Fi and modern BBS ANSI dialects to cooperate is the real challenge.</p>
</div>
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Batocera in the Living Room: One Box, Every Retro System"
section: "retro"
tags: "retro"
description: "Why the living room emulation station takes a completely different approach than the FPGA rigs upstairs."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>Batocera in the Living Room: One Box, Every Retro System</h1>
<div class="detail-content">
+27 -9
View File
@@ -1,20 +1,38 @@
---
title: "The 486 DX2-66 Tower: An Original PC Living Alongside Its FPGA Twin"
title: "The 486 DX2-66: Rebuilding a DOS Era on MiSTer FPGA"
section: "retro"
description: "Keeping an original Colani-design 486 tower running while a MiSTer core covers the same era digitally."
tags: "retro"
description: "Recreating a Vobis-Highscreen 486 DX2-66 setup on MiSTer FPGA, down to the original DOS configuration."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>The 486 DX2-66 Tower: An Original PC Living Alongside Its FPGA Twin</h1>
<h1>The 486 DX2-66: Rebuilding a DOS Era on MiSTer FPGA</h1>
<div class="detail-content">
<p>Long before Atari ST and Amiga took over the retro corner, a Vobis-Highscreen 486 DX2-66 in its distinctive Colani-designed tower case was the main PC. Decades later, it's kept around as both a museum piece and a genuine gaming machine, running alongside a MiSTer FPGA equivalent.</p>
<p>Long before Atari ST and Amiga took over the retro corner, a Vobis-Highscreen 486 DX2-66 in its distinctive Colani-designed tower case was the main PC. The original hardware is long gone, but the entire setup has been recreated 1:1 on MiSTer FPGA — same DOS version, same drivers, same AUTOEXEC.BAT and CONFIG.SYS, same directory structure.</p>
<h2>Original Hardware vs FPGA Twin</h2>
<h2>The original hardware</h2>
<p>The Vobis-Highscreen was a premium PC in its day — the Colani design gave it a distinctive curved case that still looks futuristic. Inside it ran an Intel 486 DX2 at 66MHz with 8MB of RAM, a Sound Blaster 16, and a Tseng ET4000 graphics card. A 200MB hard drive and a 3.5" floppy drive completed the setup. The machine was the go-to system for early-90s classics like Doom, Wing Commander, Duke Nukem 3D, and Lemmings.</p>
<h2>The MiSTer recreation</h2>
<p>The AO486 core on MiSTer FPGA reproduces the original hardware with near-perfect accuracy. The key was not just getting the hardware emulation right, but rebuilding the exact software environment: the same MS-DOS version, the same memory managers (HIMEM.SYS, EMM386), the same Sound Blaster drivers, and the same AUTOEXEC.BAT and CONFIG.SYS tuning that made the original machine run smoothly. Even the directory structure and file names match the original setup.</p>
<h2>Context: DOSMENU</h2>
<p>A key part of the preserved environment is <strong><a href="https://28k8.moonweb.org/bbs/pc/dosmenu/">DOSMENU</a></strong>, a boot configuration utility written in Turbo Pascal 6.0 in 1994 — originally developed specifically for this Colani 486 machine. DOSMENU let you choose at boot time which AUTOEXEC.BAT and CONFIG.SYS commands to load — a critical feature in the DOS era, where memory management was manual and games often required different driver configurations. Features included saveable presets, a built-in MOD player, PCX picture display, and a DoubleSpace memory unloader. The original source: ~50k of Turbo Pascal, roughly 1900 lines plus some inline assembler.</p>
<h2>Why a 1:1 recreation</h2>
<p>The point isn't just to play DOS games — it's to preserve the exact computing experience from that era. The way DOS memory management worked, the way sound drivers had to be configured manually, the way batch files chained together to create a usable environment. This level of detail is lost when you just launch a DOS game from a modern frontend. The MiSTer setup recreates the entire boot process, from BIOS POST to the C:\> prompt, just as it was in the mid-90s.</p>
<h2>What's preserved</h2>
<ul>
<li><strong>The original tower</strong> — an Intel 486 DX2 @ 66MHz in its iconic Colani case, the go-to machine for early-90s classics like Doom, Wing Commander, Duke Nukem 3D, and Lemmings.</li>
<li><strong>MiSTer's AO486 core</strong> — a full FPGA-based 486 emulation, configurable across variants from 386SX all the way to 486DX4, using swappable virtual hard disk (VHD) images instead of a physical drive.</li>
<li><strong>Open project</strong> — DOS-era BBS access is next on the list, likely tackled via the MiSTer core first since its networking path is simpler than wrangling period-correct ISA network cards.</li>
<li><strong>DOS boot environment</strong> — the exact AUTOEXEC.BAT and CONFIG.SYS from the original machine, including memory manager configuration and Sound Blaster IRQ settings.</li>
<li><strong>Game installations</strong> — original game installations with their setup utilities, configuration files, and saved games, all in the same directory paths as the original.</li>
<li><strong>System utilities</strong> — Norton Commander, PKZIP, the usual toolkit of the era, all configured the way they were used daily.</li>
<li><strong><a href="https://28k8.moonweb.org/bbs/pc/dosmenu/">DOSMENU</a></strong> — a custom boot configuration utility written specifically for this Colani 486 in 1994. Built in Turbo Pascal, it let you choose at boot time which AUTOEXEC.BAT and CONFIG.SYS commands to load — essential for managing DOS memory across different games and applications.</li>
<li><strong>The quirks</strong> — the specific workarounds and tweaks that were needed to get everything running together, the kind of institutional knowledge that only exists on a well-used personal machine.</li>
</ul>
<h2>AO486 vs real hardware</h2>
<p>The MiSTer AO486 core handles the emulation convincingly. Timing accuracy is good enough for games that rely on cycle-sensitive behavior. Sound Blaster emulation covers the standard OPL3 FM synthesis and digital audio. The main trade-off is that some hardware-specific quirks — like the exact timing of the ET4000 graphics card or the behavior of specific ISA bus peripherals — aren't perfectly replicated. But for the purpose of preserving a working DOS environment, it's more than sufficient.</p>
</div>
-20
View File
@@ -1,20 +0,0 @@
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData("site", { url: "https://retro.moonweb.org" });
eleventyConfig.addFilter("date", (d) => d.toISOString());
eleventyConfig.addPassthroughCopy({ "shared/theme-retro.css": "theme.css" });
eleventyConfig.addPassthroughCopy({ "shared/base.css": "shared-base.css" });
eleventyConfig.addPassthroughCopy({ "shared/favicon/retro.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({ "retro/robots.txt": "robots.txt" });
return {
dir: {
input: "retro",
includes: "../shared/_includes",
output: "dist/retro"
},
serverOptions: {
host: "0.0.0.0",
port: 8085
}
};
};
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Why an ET4000 Graphics Card and a Mega ST Don't Mix (Yet)"
section: "retro"
tags: "retro"
description: "A cautionary tale about pushing 1980s power supplies past their limits with a modern-ish graphics card."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>Why an ET4000 Graphics Card and a Mega ST Don't Mix (Yet)</h1>
<div class="detail-content">
+4 -3
View File
@@ -1,6 +1,7 @@
---
title: "Retro Computing — Vintage Hardware & FPGA | moonweb"
section: "retro"
tags: "retro"
description: "Physical retro hardware collection — Raspberry Pi units, Pi 400, and vintage computing setups."
layout: base.njk
sections:
@@ -45,8 +46,8 @@ sections:
href: "/mini-arcade-handheld-roundup/"
- heading: "DOS & PC"
cards:
- title: "💻 The 486 DX2-66 Tower"
summary: "An original Colani-design 486 tower kept running alongside its MiSTer AO486 FPGA equivalent."
- title: "💻 The 486 DX2-66"
summary: "A complete DOS environment rebuilt 1:1 on MiSTer FPGA, down to the original AUTOEXEC.BAT."
href: "/dos-486-tower/"
- heading: "Overview"
cards:
@@ -55,6 +56,6 @@ sections:
href: "/retro-corner-snapshot/"
---
<p class="site-intro">Hardware I've collected and kept running over the years. From Atari ST and Amiga to MiSTer FPGA and DOS towers.</p>
<p class="site-intro">Hardware I've collected and kept running over the years. From Atari ST and Amiga to MiSTer FPGA and DOS environments.</p>
{% include "card-grid.njk" %}
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Mini Arcades and Handhelds: Sorting the Real Deal from the Bootlegs"
section: "retro"
tags: "retro"
description: "A buyer's guide to compact retro gaming devices, and why the cheapest option is almost never the right one."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>Mini Arcades and Handhelds: Sorting the Real Deal from the Bootlegs</h1>
<div class="detail-content">
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "MiST and MiSTer: Two FPGA Boxes, Two Very Different Personalities"
section: "retro"
tags: "retro"
description: "Why the newer, more powerful MiSTer isn't automatically the better choice for every retro system."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>MiST and MiSTer: Two FPGA Boxes, Two Very Different Personalities</h1>
<div class="detail-content">
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "PiMiga 3 to 5: Chasing the Perfect Amiga-on-a-Pi Distro"
section: "retro"
tags: "retro"
description: "Three generations of PiMiga later, here's what actually changed — and why newer isn't always better."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>PiMiga 3 to 5: Chasing the Perfect Amiga-on-a-Pi Distro</h1>
<div class="detail-content">
+11 -1
View File
@@ -1,9 +1,10 @@
---
title: "The Retro Corner Today: A Snapshot of the Whole Setup"
section: "retro"
tags: "retro"
description: "A tour through every retro system currently active across the home, room by room."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>The Retro Corner Today: A Snapshot of the Whole Setup</h1>
<div class="detail-content">
@@ -18,5 +19,14 @@ parent: "/"
<li><strong>The focus, unmistakably</strong> — Amiga and Atari ST are the heart of the collection; everything else (DOS, arcade, handhelds) exists to fill in the gaps around those two platforms.</li>
</ul>
<h2>The office setup in detail</h2>
<p>The office houses the main retro computing workstation. The three Mega ST 4 machines each have a defined role: one runs GEM-based productivity software, one is set up for MIDI music production with a connected synthesizer, and one serves as the development machine for ST software. The Mega ST 2 is the backup and testing machine. The MiSTer FPGA handles Amiga and DOS emulation with near-perfect timing accuracy, while the MiST provides a dedicated Atari ST experience without the overhead of the MiSTer's larger feature set.</p>
<h2>The living room setup</h2>
<p>The Batocera box in the living room takes a completely different approach — it's not about hardware authenticity, it's about convenience. A compact PC boots directly into the Batocera frontend, providing a console-like experience for casual retro gaming on the TV. Controllers are wireless, the interface is controller-friendly, and the focus is on games that work well from a couch rather than demos or productivity software that needs a keyboard and mouse.</p>
<h2>How it all connects</h2>
<p>All machines share the same network infrastructure, with FTP access to the NAS for file transfers. Disk images and software archives are stored centrally and served to individual machines as needed. The MiSTer and MiST both use SD cards for storage, while the original hardware uses CompactFlash adapters or SCSI-to-IDE bridges depending on the machine. The Raspberry Pi 400 accesses the same NFS shares as the other Pis in the flat.</p>
<p class="redacted-note">The honest status update: several open items remain — documenting the exact handheld models, and waiting on the A1200 Mini pre-order. Retro computing, it turns out, is never really "done."</p>
</div>
-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://retro.moonweb.org/sitemap.xml
-5
View File
@@ -1,5 +0,0 @@
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
{% include "sitemap.njk" %}
+32 -1
View File
@@ -1,9 +1,10 @@
---
title: "The A1200: What We Know Before It Ships"
section: "retro"
tags: "retro"
description: "A close look at Retro Games Ltd.'s upcoming Amiga 1200 console, based on everything public so far."
layout: base.njk
parent: "/"
parent: "/retro/"
---
<h1>The A1200: What We Know Before It Ships</h1>
<div class="detail-content">
@@ -18,6 +19,36 @@ parent: "/"
<li><strong>Expected internals</strong> — likely an Allwinner ARM SoC running Amiberry underneath (following the pattern of the earlier A500 Mini), rather than genuine 68EC020 silicon.</li>
</ul>
<h2>Context: where this fits in the retro landscape</h2>
<p>The A1200 sits in a growing market of mini-consoles and FPGA recreations.
Unlike MiSTer FPGA, which aims for cycle-accurate hardware reproduction,
the A1200 is a software emulation box — similar to the A500 Mini that
Retro Games Ltd. released in 2022. The A500 Mini was well-received but
had its quirks: limited game selection, no keyboard support, and an
emulation layer that didn't quite match the original hardware in all
cases. The A1200 appears to address some of these criticisms, particularly
with the keyboard and expanded I/O.</p>
<h2>How it compares to alternatives</h2>
<p>For someone who wants to run Amiga software today, there are several
options. A real A1200 with a compact flash adapter and a modern LCD
monitor is the purist approach, but requires maintenance and debugging.
MiSTer FPGA offers near-perfect hardware reproduction but costs
significantly more. Amiberry on a Raspberry Pi is the budget option,
with good compatibility but occasional timing issues. The A1200
positions itself as a plug-and-play alternative — set it up in minutes,
no configuration required, at the cost of some flexibility.</p>
<h2>What this means for the Amiga community</h2>
<p>If the A1200 ships on time and delivers on the promised feature set,
it could become the default entry point for people curious about the
Amiga. The original hardware is getting harder to find in good condition,
and the capacitor-bomb motherboard issue means that even working units need
recapping. A modern, reliable reproduction — even if it's software
emulation — lowers the barrier significantly. The 25 pre-installed games
give newcomers a curated starting point, and the USB sideloading support
means the library can be expanded without hardware modifications.</p>
<p class="redacted-note">Worth a heads-up if you're pre-ordering: not every regional shop went live on launch day, and lesser-known international retailers sometimes get product photos and details before the official site does. With a seven-month wait to delivery, there's no rush to jump on the very first listing you find.</p>
</div>
+72
View File
@@ -0,0 +1,72 @@
[
{
"expression": "http.host eq \"hub.moonweb.org\"",
"description": "hub.moonweb.org -> www.moonweb.org",
"action": "redirect",
"action_parameters": {
"from_value": {
"target_url": {
"expression": "concat(\"https://www.moonweb.org\", http.request.uri.path)"
},
"status_code": 301,
"preserve_query_string": true
}
}
},
{
"expression": "http.host eq \"infra.moonweb.org\"",
"description": "infra.moonweb.org -> www.moonweb.org/infra/",
"action": "redirect",
"action_parameters": {
"from_value": {
"target_url": {
"expression": "concat(\"https://www.moonweb.org/infra\", http.request.uri.path)"
},
"status_code": 301,
"preserve_query_string": true
}
}
},
{
"expression": "http.host eq \"smarthome.moonweb.org\"",
"description": "smarthome.moonweb.org -> www.moonweb.org/smarthome/",
"action": "redirect",
"action_parameters": {
"from_value": {
"target_url": {
"expression": "concat(\"https://www.moonweb.org/smarthome\", http.request.uri.path)"
},
"status_code": 301,
"preserve_query_string": true
}
}
},
{
"expression": "http.host eq \"code.moonweb.org\"",
"description": "code.moonweb.org -> www.moonweb.org/code/",
"action": "redirect",
"action_parameters": {
"from_value": {
"target_url": {
"expression": "concat(\"https://www.moonweb.org/code\", http.request.uri.path)"
},
"status_code": 301,
"preserve_query_string": true
}
}
},
{
"expression": "http.host eq \"retro.moonweb.org\"",
"description": "retro.moonweb.org -> www.moonweb.org/retro/",
"action": "redirect",
"action_parameters": {
"from_value": {
"target_url": {
"expression": "concat(\"https://www.moonweb.org/retro\", http.request.uri.path)"
},
"status_code": 301,
"preserve_query_string": true
}
}
}
]
+172
View File
@@ -0,0 +1,172 @@
#!/bin/bash
# Setup Cloudflare redirect rules for moonweb.org
# Redirects old subdomains to www.moonweb.org subdirectories
#
# Required env vars:
# CLOUDFLARE_API_TOKEN - API token with Dynamic Redirects Write permission
# CLOUDFLARE_ZONE_ID - Zone ID for moonweb.org
#
# Usage:
# ./setup-redirects.sh # Apply redirects
# ./setup-redirects.sh --dry-run # Show what would be created
# ./setup-redirects.sh --status # Show current redirect rules
# ./setup-redirects.sh --delete # Delete all redirect rules
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RULES_FILE="$SCRIPT_DIR/redirect-rules.json"
API="https://api.cloudflare.com/client/v4"
PHASE="http_request_dynamic_redirect"
RULESET_NAME="moonweb-subdomain-redirects"
DRY_RUN=false
STATUS=false
DELETE=false
for arg in "$@"; do
case $arg in
--dry-run) DRY_RUN=true ;;
--status) STATUS=true ;;
--delete) DELETE=true ;;
esac
done
if [ -z "${CLOUDFLARE_API_TOKEN:-}" ] || [ -z "${CLOUDFLARE_ZONE_ID:-}" ]; then
echo "Error: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID must be set"
exit 1
fi
cf_api() {
local method=$1
local url=$2
shift 2
curl -s -X "$method" "$url" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
"$@"
}
# Get existing ruleset ID for this phase
get_ruleset_id() {
cf_api GET "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets" | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for r in data.get('result', []):
if r.get('phase') == '$PHASE':
print(r['id'])
sys.exit(0)
print('')
" 2>/dev/null
}
# Get rules in a ruleset
get_rules() {
local ruleset_id=$1
cf_api GET "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets/$ruleset_id" | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for r in data.get('result', {}).get('rules', []):
desc = r.get('description', 'unnamed')
expr = r.get('expression', '')
print(f' {desc}')
print(f' expression: {expr}')
" 2>/dev/null
}
# Show current status
if [ "$STATUS" = true ]; then
echo "=== Current redirect rules ==="
ruleset_id=$(get_ruleset_id)
if [ -z "$ruleset_id" ]; then
echo "No redirect ruleset found for phase $PHASE"
exit 0
fi
echo "Ruleset ID: $ruleset_id"
get_rules "$ruleset_id"
exit 0
fi
# Delete all redirect rules
if [ "$DELETE" = true ]; then
echo "=== Deleting redirect rules ==="
ruleset_id=$(get_ruleset_id)
if [ -z "$ruleset_id" ]; then
echo "No redirect ruleset found, nothing to delete"
exit 0
fi
echo "Deleting ruleset $ruleset_id..."
RESULT=$(cf_api DELETE "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets/$ruleset_id")
echo "$RESULT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if data.get('success'):
print('Deleted successfully')
else:
for e in data.get('errors', []):
print(f\"Error: {e.get('message', '')}\")
" 2>/dev/null
exit 0
fi
# Build payload from JSON file
PAYLOAD=$(python3 -c "
import json
with open('$RULES_FILE') as f:
rules = json.load(f)
payload = {
'name': '$RULESET_NAME',
'kind': 'zone',
'phase': '$PHASE',
'rules': rules
}
print(json.dumps(payload))
")
RULE_COUNT=$(echo "$PAYLOAD" | python3 -c "import sys, json; print(len(json.load(sys.stdin)['rules']))" 2>/dev/null)
if [ "$DRY_RUN" = true ]; then
echo "=== DRY RUN ==="
echo "Would create/update ruleset '$RULESET_NAME' with $RULE_COUNT rules:"
python3 -c "
import json
with open('$RULES_FILE') as f:
rules = json.load(f)
for r in rules:
print(f\" - {r['description']}\")
" 2>/dev/null
exit 0
fi
# Check if ruleset already exists
echo "=== Checking existing ruleset ==="
EXISTING_ID=$(get_ruleset_id)
if [ -n "$EXISTING_ID" ]; then
echo "Updating existing ruleset $EXISTING_ID..."
RESULT=$(cf_api PUT "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets/$EXISTING_ID" \
-d "$PAYLOAD")
else
echo "Creating new ruleset..."
RESULT=$(cf_api POST "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets" \
-d "$PAYLOAD")
fi
# Check result
echo "$RESULT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if data.get('success'):
ruleset = data['result']
print(f\"Success! Ruleset: {ruleset['name']} (ID: {ruleset['id']})\")
print(f\"Rules: {len(ruleset.get('rules', []))}\")
for r in ruleset.get('rules', []):
print(f\" - {r.get('description', 'unnamed')}\")
else:
print('Error:')
for e in data.get('errors', []):
print(f\" {e.get('code', '')}: {e.get('message', '')}\")
sys.exit(1)
" 2>/dev/null
+10 -11
View File
@@ -17,12 +17,11 @@
<meta name="twitter:title" content="{{ title }}{% if 'moonweb' not in title and 'stefankoelle' not in title %} · moonweb.org{% endif %}">
<meta name="twitter:description" content="{{ description }}">
{% if ogImage %}<meta name="twitter:image" content="{{ site.url }}{{ ogImage }}">{% endif %}
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lobster&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/shared-base.css">
<link rel="stylesheet" href="/theme.css">
<style>:root { --accent: {% if section == 'infra' %}#99333A{% elif section == 'smarthome' %}#1f8a8a{% elif section == 'code' %}#3E5098{% elif section == 'retro' %}#8a6d3b{% else %}#3b6ea5{% endif %}; }</style>
<script type="application/ld+json">
{
"@context": "https://schema.org",
@@ -45,8 +44,8 @@
"@context": "https://schema.org",
"@type": "Person",
"name": "Stefan Koelle",
"url": "https://hub.moonweb.org",
"image": "https://hub.moonweb.org/stefan-koelle-foto.jpg",
"url": "https://www.moonweb.org",
"image": "https://www.moonweb.org/stefan-koelle-foto.jpg",
"jobTitle": "Senior Software Developer & Architect",
"address": {
"@type": "PostalAddress",
@@ -69,11 +68,11 @@
<span class="site-title">{{ section }}.moonweb.org</span>
</div>
<nav class="site-switcher">
<a href="https://hub.moonweb.org">hub</a>
<a href="https://infra.moonweb.org">infra</a>
<a href="https://smarthome.moonweb.org">smarthome</a>
<a href="https://code.moonweb.org">code</a>
<a href="https://retro.moonweb.org">retro</a>
<a href="/">hub</a>
<a href="/infra/">infra</a>
<a href="/smarthome/">smarthome</a>
<a href="/code/">code</a>
<a href="/retro/">retro</a>
<a href="https://stefankoelle.de">cv</a>
</nav>
</header>
@@ -84,8 +83,8 @@
</main>
<footer class="site-footer">
<p>Part of the <a href="https://hub.moonweb.org">moonweb.org</a> homelab.</p>
<p><a href="https://hub.moonweb.org/impressum/">Legal Notice &amp; Privacy Policy</a></p>
<p>Part of the <a href="/">moonweb.org</a> homelab.</p>
<p><a href="/impressum/">Legal Notice &amp; Privacy Policy</a></p>
</footer>
</body>
</html>
+27
View File
@@ -214,3 +214,30 @@ main { max-width: 1100px; margin: 0 auto; padding: 2rem; }
color: #666;
margin: 0 0 2rem;
}
.about-expanded {
margin-top: 3rem;
max-width: 800px;
}
.about-expanded h2 {
font-size: 1.1rem;
color: var(--accent);
margin: 2rem 0 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--card-border);
}
.about-expanded h2:first-child {
margin-top: 0;
}
.about-expanded p {
font-size: 0.95rem;
color: #444;
line-height: 1.7;
margin: 0 0 1rem;
}
.about-expanded a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.about-expanded a:hover { color: var(--text); }
+2 -1
View File
@@ -1,9 +1,10 @@
---
title: "Page Not Found"
section: "smarthome"
tags: "smarthome"
description: "The page you are looking for does not exist."
layout: base.njk
permalink: /404.html
permalink: /smarthome/404.html
---
<div class="error-page">
<h1>404</h1>
+39
View File
@@ -0,0 +1,39 @@
---
title: "Air Quality Dashboard"
section: "smarthome"
tags: "smarthome"
parent: "/smarthome/"
description: "Real-time temperature and humidity monitoring across all rooms, powered by InfluxDB and Chart.js."
layout: base.njk
---
<h1>Air Quality Dashboard</h1>
<div class="detail-content">
<p>A custom HTML5 dashboard that displays real-time temperature and humidity readings from sensors across the apartment. It consumes data from InfluxDB via the Flux query API and visualizes it with Chart.js, auto-refreshing every 30 seconds.</p>
<h2>Architecture</h2>
<p>The dashboard sits at the end of a sensor data pipeline that starts at physical devices scattered around the flat:</p>
<ul>
<li><strong>Sensors</strong> HomematicIP room sensors and ESP32 devices with DHT22 sensors publish temperature and humidity readings via MQTT.</li>
<li><strong>MQTT broker</strong> Mosquitto receives the per-device topics and makes them available for subscription.</li>
<li><strong>Write layer</strong> A small subscription service picks up the MQTT messages and writes them into InfluxDB 2.x, tagged by room, device, and measurement type.</li>
<li><strong>InfluxDB</strong> Stores the time-series data in a <code>sensoren</code> bucket, organized by org <code>Heimnetzwerk</code>.</li>
<li><strong>Dashboard</strong> A static HTML page queries InfluxDB via Flux every 30 seconds, rendering current values and 24-hour historical charts per room.</li>
</ul>
<h2>Rooms covered</h2>
<p>The dashboard monitors 10 rooms across the apartment: server room, balcony, living room (desk and iMac areas), kitchen, bathroom, hallway, study, children's room, and bedroom. Each room shows current temperature and humidity, with optional pressure and light-level readings where sensors support it.</p>
<h2>Dashboard features</h2>
<ul>
<li><strong>Current values grid</strong> at-a-glance temperature and humidity for every room.</li>
<li><strong>Historical charts</strong> 24-hour temperature and humidity graphs per room with min/max stats.</li>
<li><strong>Auto-refresh</strong> Data updates every 30 seconds without page reload.</li>
<li><strong>Error handling</strong> Graceful fallback when InfluxDB is unreachable, with loading indicators during initial fetch.</li>
<li><strong>Responsive design</strong> Works on phones, tablets, and desktop browsers.</li>
</ul>
<h2>Relationship to other dashboards</h2>
<p>The air quality dashboard is a standalone page, separate from the main home dashboard and the Tasmota energy monitoring setup. It shares the same InfluxDB instance and MQTT infrastructure but queries different measurement types (temperature/humidity vs. power/energy). The home dashboard shows a summary of room temperatures alongside energy data, while this dashboard provides the detailed view with historical charts for climate monitoring specifically.</p>
</div>
+2 -1
View File
@@ -1,7 +1,8 @@
---
title: "AirPlay Audio"
section: "smarthome"
parent: "/"
tags: "smarthome"
parent: "/smarthome/"
description: "Multi-room AirPlay receivers with software volume boost on Raspberry Pi devices."
layout: base.njk
---
+17 -2
View File
@@ -1,7 +1,8 @@
---
title: "Balkon Pi"
section: "smarthome"
parent: "/"
tags: "smarthome"
parent: "/smarthome/"
description: "Balcony sensor, lighting, and audio automation running on a Raspberry Pi Zero W."
layout: base.njk
---
@@ -11,7 +12,10 @@ layout: base.njk
<p>A Raspberry Pi Zero W mounted near the balcony acts as a small,
self-contained smart-balcony controller: light control, an internet
radio player, sensor readings, and its own backup/monitoring, all on
very modest hardware.</p>
very modest hardware. The goal is to make the balcony a more pleasant
space — light control when it gets dark, background music while sitting
outside, and temperature readings to know what to expect before stepping
out.</p>
<h2>What it does</h2>
<ul>
@@ -21,6 +25,16 @@ very modest hardware.</p>
<li><strong>Internet radio</strong> via a lightweight command-line audio player, controlled by a small custom tool ported from an earlier bathroom-Pi project.</li>
</ul>
<h2>Hardware details</h2>
<p>The Pi Zero W is connected to an ESP32 microcontroller that handles
the actual light switching via a relay module. Communication happens
over HTTP — the Pi sends a simple request to toggle the relay, and the
ESP32 responds with the current light state. The ESP32 also reads a
DHT22 temperature and humidity sensor, publishing the values to MQTT
for the home dashboard. This split makes sense: the Pi handles the
user-facing logic (audio, buttons, internet radio), while the ESP32
handles the real-time hardware control.</p>
<h2>Monitoring & backup, even on tiny hardware</h2>
<p>Despite the very limited RAM, this Pi still participates in the same
Prometheus monitoring pattern as the rest of the fleet — with the metrics
@@ -33,6 +47,7 @@ configuration to the NAS over the internal network.</p>
<ul>
<li>Adding a USB webcam stream, mirroring the pattern already used for the 3D printer.</li>
<li>Migrating onto the planned isolated IoT network segment once that's fully set up.</li>
<li>Adding weather data from the DHT22 sensor to the LED matrix displays around the flat.</li>
</ul>
</div>
-20
View File
@@ -1,20 +0,0 @@
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData("site", { url: "https://smarthome.moonweb.org" });
eleventyConfig.addFilter("date", (d) => d.toISOString());
eleventyConfig.addPassthroughCopy({ "shared/theme-smarthome.css": "theme.css" });
eleventyConfig.addPassthroughCopy({ "shared/base.css": "shared-base.css" });
eleventyConfig.addPassthroughCopy({ "shared/favicon/smarthome.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({ "smarthome/robots.txt": "robots.txt" });
return {
dir: {
input: "smarthome",
includes: "../shared/_includes",
output: "dist/smarthome"
},
serverOptions: {
host: "0.0.0.0",
port: 8083
}
};
};
+12 -4
View File
@@ -1,9 +1,10 @@
---
title: "Home Control Buttons"
section: "smarthome"
tags: "smarthome"
description: "Overview of the central buttons dashboard used to control lighting, smart plugs, music, network devices and monitoring shortcuts around the apartment."
layout: base.njk
parent: "/"
parent: "/smarthome/"
---
<h1>Home Control Buttons</h1>
<div class="detail-content">
@@ -29,14 +30,21 @@ parent: "/"
<li><strong>Single sockets (SP112)</strong> — four individually switchable outlets covering hallway, kitchen, dryer and washing machine circuits, each also powering an associated LED matrix or small Pi.</li>
<li><strong>Other plug families</strong> — additional Nous, Eightree (ESP32-based), Athom and IDS smart plugs cover spare capacity, living-room seating/desk outlets, storage room, kitchen appliances, and a few legacy TV/PC outlets (several currently marked defective).</li>
</ul>
<p>The plug collection has grown over time as different brands became available at different price points. Gosund P1 multi-socket strips handle areas with multiple devices, while the SP112 single sockets cover dedicated appliances. Eightree plugs are ESP32-based and flashable to Tasmota, while the Athom and IDS models round out the remaining circuits. A Tasmota RF Bridge extends the ecosystem to RF-only devices like the 3D printer plug in the study.</p>
<h2>Special devices & status</h2>
<h2>Special devices &amp; status</h2>
<ul>
<li><strong>Tasmota RF Bridge / Delock</strong> — bridges RF-only devices (like the home-office 3D printer plug) into the Tasmota/MQTT ecosystem.</li>
<li><strong>LED Matrix restarts</strong> — one-click restart buttons for each room's LED matrix display, avoiding a manual power-cycle.</li>
<li><strong>Healthchecks & version info</strong> — direct links into the Healthchecks dashboard and a version/status overview for the connected devices.</li>
<li><strong>LED Matrix restarts</strong> — one-click restart buttons for each of the six room LED matrix displays (living room, study, bathroom, kitchen, hallway, Nepomuk room), avoiding a manual power-cycle.</li>
<li><strong>Healthchecks &amp; version info</strong> — direct links into the Healthchecks dashboard and a version/status overview for the connected devices.</li>
</ul>
<h2>Calendar &amp; weather</h2>
<p>The home dashboard also includes two additional views beyond the button controls: a calendar page showing the next 10 upcoming appointments (powered by the <a href="https://github.com/skoelle/calender_sync">Google Calendar Sync</a> service), and a weather page displaying hourly forecasts for the local area. Both are accessed from the same landing page as the button controls.</p>
<h2>Music library</h2>
<p>A Music Stretto instance running on the Synology provides access to the local MP3 library. It's reachable from the same home dashboard landing page and used for quick playback without needing to open a dedicated music app.</p>
<h2>Network shortcuts</h2>
<ul>
<li><strong>Routers & mesh</strong> — quick links to the main Fritzbox and its mesh repeaters covering different floors and the kitchen/IoT band.</li>
+30 -1
View File
@@ -1,7 +1,8 @@
---
title: "HomematicIP + MQTT"
section: "smarthome"
parent: "/"
tags: "smarthome"
parent: "/smarthome/"
description: "How HomematicIP room sensors are bridged into MQTT and stored in InfluxDB for dashboards."
layout: base.njk
---
@@ -29,11 +30,39 @@ sensor state changes to MQTT.</p>
<li>Grafana reads from InfluxDB for the temperature/humidity dashboards.</li>
</ol>
<h2>The Home Assistant automations</h2>
<p>Each room has its own automation in Home Assistant that listens for
state changes on the corresponding HomematicIP thermostat entity. When
the temperature or humidity changes, the automation publishes the new
values to MQTT topics like <code>homematic/livingroom/temperature</code>
and <code>homematic/livingroom/humidity</code>. The automations are
simple YAML configurations, not custom code, which makes them easy to
maintain and debug.</p>
<h2>The InfluxDB writer</h2>
<p>A small .NET background service runs as a Docker container and
subscribes to all <code>homematic/+/+</code> MQTT topics. When a
message arrives, it parses the room name and measurement type from the
topic, converts the value to a float, and writes it into InfluxDB with
appropriate tags. The service handles connection drops and reconnection
automatically, and logs all write operations for debugging.</p>
<h2>Dashboard integration</h2>
<p>Grafana queries InfluxDB using Flux to display real-time temperature
and humidity charts for each room. The dashboards show current values,
24-hour trends, and historical averages. This data is also used in the
home dashboard to display room temperatures alongside other sensor data
like air quality and power consumption.</p>
<h2>Why this is worth documenting</h2>
<ul>
<li>No extra Docker container or unmaintained library dependency.</li>
<li>Cloud-API changes are absorbed by Home Assistant's own maintainers, not by custom code.</li>
<li>The MQTT bridge is just configuration (automations), not a service that needs its own uptime monitoring.</li>
<li>The entire chain is observable: MQTT topics can be monitored with any MQTT client, InfluxDB has its own explorer, and Grafana dashboards show the data flow end-to-end.</li>
</ul>
<h2>Lessons learned</h2>
<p>The original approach of running a dedicated exporter was more "architecturally pure" but fragile in practice. When the Python library stopped working, the entire sensor pipeline broke. The Home Assistant approach is more robust because it leverages a well-maintained integration and uses MQTT as a simple, well-understood transport layer. The lesson: for home automation, prefer integrations that are actively maintained over custom code that needs constant attention.</p>
</div>
+23 -2
View File
@@ -1,6 +1,7 @@
---
title: "Smart Home Projects — Home Assistant, MQTT & IoT | moonweb"
section: "smarthome"
tags: "smarthome"
description: "Smart home projects: Home Assistant, MQTT sensors, AirPlay audio, OctoPrint, and energy monitoring."
layout: base.njk
sections:
@@ -36,6 +37,10 @@ sections:
summary: "ESP32 dashboard showing weather, calendar, and MVG departures."
href: "https://github.com/skoelle/m5stack-dashboard"
emoji: "🖥️"
- title: "Air quality dashboard"
summary: "Real-time temperature and humidity monitoring across all rooms with historical charts."
href: "/air-quality/"
emoji: "🌡️"
- heading: "Personal services"
cards:
- title: "iCloud Contacts Sync"
@@ -64,11 +69,27 @@ sections:
summary: "Remote 3D printer control and camera streaming."
href: "/octoprint/"
emoji: "🖨️"
- title: "TubeArchivist"
summary: "Self-hosted YouTube archiving running on the Synology."
- title: "YouTube archiving"
summary: "TubeArchivist and TubeSync for self-hosted YouTube downloading on the Synology."
href: "/tubearchivist/"
emoji: "📺"
- title: "Kids RFID MP3 Player"
summary: "Arduino-based audio player for kids using RFID cards, built as a custom alternative to Hoerbert."
href: "/kids-rfid-player/"
emoji: "🎵"
---
<p class="site-intro">The heart of the homelab. Home Assistant, MQTT sensors, dashboards, and automation projects that make everyday life a little more interesting.</p>
{% include "card-grid.njk" %}
<section class="about-expanded">
<h2>The stack</h2>
<p>Everything connects through MQTT. HomematicIP thermostats report room temperatures via Home Assistant automations, Tasmota smart plugs feed energy data, and various ESP32 and Raspberry Pi devices publish sensor readings. InfluxDB stores the time-series data, and Grafana dashboards visualize it all in real time.</p>
<p>The home dashboard runs on multiple touch displays around the flat: an ESP32-S3 WT32-SC01 in the living room and an M5Stack in the study. Both show room temperatures, energy usage, and quick-action buttons for lighting and music control.</p>
<h2>Automation philosophy</h2>
<p>The goal is subtle automation that improves daily life without being intrusive. Lights turn on when someone enters a room, music starts playing in the bathroom when the light switches on, and LED matrix displays show the time, weather, and laundry status. But nothing talks unless spoken to — no voice assistants, no always-on microphones, just simple sensor-driven automation that runs locally without cloud dependencies.</p>
<h2>Self-hosted services</h2>
<p>Beyond hardware automation, several self-hosted services run on the Synology NAS: TubeArchivist and TubeSync for YouTube archiving, OctoPrint for remote 3D printer control, iCloud Contacts Sync for keeping the address book up to date, and a Google Calendar sync for dashboard widgets. Each service is containerized and backed up as part of the NAS backup strategy.</p>
</section>
+53
View File
@@ -0,0 +1,53 @@
---
title: "Kids RFID MP3 Player"
section: "smarthome"
tags: "smarthome"
parent: "/smarthome/"
description: "A custom Arduino-based RFID audio player for kids, inspired by the Hoerbert and Tonuino projects."
layout: base.njk
---
<h1>Kids RFID MP3 Player</h1>
<div class="detail-content">
<p>An Arduino-based MP3 player where kids select audio content by holding RFID cards against the device. No screen, no menus, no WiFi — just tap a card and music plays. Built together with my son in 2018 as a cheaper, more flexible alternative to commercial products like the Hoerbert or Tonuino.</p>
<h2>Why this exists</h2>
<p>Commercial kids' audio players have trade-offs. The Hoerbert only offers 9 direct-select buttons with color-coded cards that aren't very intuitive for very young children. The Tonuino uses expensive RFID figures and relies on WiFi streaming, which introduces latency and dependency on network availability. This project aimed for the best of both worlds: RFID-based content selection with local-only playback, analog volume control, and minimal buttons.</p>
<h2>Hardware</h2>
<ul>
<li><strong>Arduino</strong> — the microcontroller running the custom firmware.</li>
<li><strong>RFID module</strong> — reads MIFARE cards to trigger audio playback.</li>
<li><strong>DFPlayer Mini</strong> — compact MP3 player module with built-in amplifier, handling audio decoding and playback directly from an SD card.</li>
<li><strong>Pololu power switch</strong> — handles auto-shutdown to prevent battery drain, replacing a manual toggle switch that a child would forget to turn off.</li>
<li><strong>Analog volume knob</strong> — a rotary potentiometer for tactile volume control, deliberately chosen over digital buttons.</li>
<li><strong>3 LEDs</strong> — green (playing), yellow (paused), red (stopped/at end).</li>
</ul>
<h2>How it works</h2>
<p>Each RFID card is mapped to a specific audio file on the SD card. Hold a card against the reader and the corresponding story or song starts playing. Hold the RFID keychain against the reader to pause; hold it again to resume. No other buttons exist on the device — the design intentionally minimizes interaction points so even a toddler can use it independently.</p>
<h2>Auto-shutdown behavior</h2>
<p>The power management distinguishes between two states to avoid unnecessary battery drain:</p>
<ul>
<li><strong>Stopped</strong> (playback reached the end) — the device shuts down after 5 minutes.</li>
<li><strong>Paused</strong> (user paused via RFID) — the device stays on for 1 hour before shutting down, allowing easy resumption.</li>
</ul>
<h2>The case</h2>
<p>The enclosure is a custom-built retro-style wooden box, painted by hand. The name "Rocky Box" came from my son, who painted a Paw Patrol character on the front. The design is deliberately quirky and homemade rather than polished — it has character.</p>
<h2>What's missing (by design)</h2>
<ul>
<li>No resume after power-off — turning the device off mid-playback means starting the story over from the beginning.</li>
<li>No skip forward/backward within a track — stories play start to finish.</li>
</ul>
<p>Both omissions are intentional. Young children don't need seeking functionality, and keeping the interface to just "tap card, listen" reduces confusion.</p>
<h2>Setup</h2>
<p>All 100 RFID cards were pre-programmed once and labeled with numbers. No card learning or reprogramming is needed after initial setup — just hand a child a numbered card and they know which story it triggers.</p>
<h2>Code</h2>
<p>The Arduino firmware is not yet published on GitHub. It's a custom implementation (not Tonuino), written in C++ for the Arduino platform. The code may be uploaded in the future.</p>
</div>
+24 -3
View File
@@ -1,7 +1,8 @@
---
title: "OctoPrint"
section: "smarthome"
parent: "/"
tags: "smarthome"
parent: "/smarthome/"
description: "Remote 3D printer control and camera streaming with OctoPrint on Raspberry Pi."
layout: base.njk
---
@@ -10,7 +11,9 @@ layout: base.njk
<p>A Raspberry Pi 3B runs OctoPrint for remote 3D-printer control, plus a
live camera stream so print progress can be checked without walking over
to the printer.</p>
to the printer. This eliminates the need to stand next to the printer
watching the first layer — a quality-of-life improvement that's hard to
go back from once you've experienced it.</p>
<h2>Current setup</h2>
<ul>
@@ -19,6 +22,14 @@ to the printer.</p>
<li><strong>Container management</strong> (Portainer, cAdvisor, a node exporter) runs alongside, giving the same monitoring pattern as the rest of the fleet.</li>
</ul>
<h2>Camera and streaming</h2>
<p>The camera setup uses a standard USB webcam connected to the Pi. The
camera-server project provides two streaming modes: MJPEG for the
OctoPrint dashboard (low latency, works in any browser) and HLS for
remote viewing over slower connections. The MJPEG stream is the primary
way to check on prints — it loads quickly and updates in real time in the
OctoPrint web interface.</p>
<h2>History</h2>
<p>This Pi originally also had a small status display attached; the
display was later moved to the backup Pi (which benefits more from local,
@@ -30,6 +41,16 @@ that point to better reflect its now-singular focus on the printer.</p>
<p>After moving this Pi to a newer OS release, boot configuration files
moved to a new path — editing the old path silently does nothing, which
is an easy trap when following older notes or tutorials for the same
hardware.</p>
hardware. The lesson: always verify that configuration changes actually
take effect, especially after OS upgrades on embedded hardware.</p>
<h2>Why Docker on a Pi</h2>
<p>Running OctoPrint in Docker on a Pi 3B might seem like overkill, but
it provides consistency with the rest of the fleet. The same Portainer
interface manages this container alongside the NAS services, the same
monitoring stack collects its metrics, and the same backup strategy
covers its configuration. The overhead of Docker on ARM is minimal for a
single-container setup, and the operational benefits justify the small
resource cost.</p>
</div>
-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://smarthome.moonweb.org/sitemap.xml
-5
View File
@@ -1,5 +0,0 @@
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
{% include "sitemap.njk" %}
+27 -2
View File
@@ -1,7 +1,8 @@
---
title: "Tasmota & Energy Monitoring"
section: "smarthome"
parent: "/"
tags: "smarthome"
parent: "/smarthome/"
description: "Smart plugs and power monitoring across the flat with daily usage statistics."
layout: base.njk
---
@@ -10,7 +11,9 @@ layout: base.njk
<p>Power monitoring across the flat runs on a mix of Tasmota-flashed smart
plugs, tracking accumulated kWh per device and feeding daily usage
figures into the home dashboard.</p>
figures into the home dashboard. The setup uses 8 Tasmota-enabled devices
covering kitchen appliances, office equipment, entertainment systems,
and LED matrix displays.</p>
<h2>What's metered</h2>
<table>
@@ -35,11 +38,33 @@ well-documented community process. It occasionally fails on the first
attempt due to a transient Wi-Fi handshake issue — retrying resolves it
without any special handling.</p>
<h2>How the data flows</h2>
<p>Each Tasmota plug publishes its power readings to MQTT via the
Mosquitto broker running on the Docker host. A small subscription
service picks up the per-device topics and writes the values into
InfluxDB 2.x, tagged by device name, room, and measurement type
(power, energy, voltage). The home dashboard then queries InfluxDB
via Flux to display real-time and historical usage charts.</p>
<h2>What this data is used for</h2>
<ul>
<li>Spotting appliances with unexpectedly high standby draw.</li>
<li>Sanity-checking that "turned off" devices are actually drawing near-zero power.</li>
<li>Feeding the home dashboard's live power figures alongside the climate sensors.</li>
<li>Tracking long-term energy trends to identify efficiency improvements.</li>
<li>Alerting when a device draws significantly more than its baseline (e.g., a failing appliance).</li>
</ul>
<h2>Air quality and climate data</h2>
<p>The same InfluxDB instance also stores temperature and humidity readings from HomematicIP room sensors and ESP32 DHT22 devices. These feed the <a href="/air-quality/">air quality dashboard</a>, which visualizes 24-hour climate history across 10 rooms. The energy and climate data share the MQTT-to-InfluxDB pipeline but live in separate measurement categories, keeping power monitoring and environmental sensing cleanly separated.</p>
<h2>Why Tasmota over alternatives</h2>
<p>Tasmota was chosen over alternatives like ESPHome or Tuya firmware for
several reasons. The local-only control means no cloud dependency — the
plugs work even if the internet is down. The MQTT integration is mature
and well-documented. The energy monitoring sensors are accurate enough
for home use (typically within 5% of a dedicated energy meter). And the
firmware supports a wide range of hardware, making it easy to find
compatible plugs at reasonable prices.</p>
</div>
+25 -17
View File
@@ -1,36 +1,44 @@
---
title: "TubeArchivist"
title: "YouTube Archiving: TubeArchivist & TubeSync"
section: "smarthome"
parent: "/"
description: "Self-hosted YouTube archiving and media management running on the Synology NAS."
tags: "smarthome"
parent: "/smarthome/"
description: "Self-hosted YouTube archiving and media management running on the Synology NAS, comparing TubeArchivist and TubeSync."
layout: base.njk
---
<h1>TubeArchivist</h1>
<h1>YouTube Archiving: TubeArchivist &amp; TubeSync</h1>
<div class="detail-content">
<p>TubeArchivist runs on the Synology NAS via Docker Compose, self-hosting
a searchable archive of downloaded YouTube content (Elasticsearch +
Redis + the TubeArchivist app itself).</p>
<p>Two self-hosted YouTube tools run on the Synology NAS via Docker Compose, each taking a different approach to the same goal: keeping offline copies of channels worth rewatching. TubeArchivist provides a full archive with search and metadata, while TubeSync focuses on automatic synchronization of playlists into a media server library.</p>
<h2>Why this needed real troubleshooting</h2>
<p>Running Elasticsearch on Synology's DSM kernel isn't fully
straightforward: newer Elasticsearch releases require a Linux kernel
feature (SECCOMP) that Synology's kernel doesn't compile in. The fix is
version-pinning Elasticsearch to the last release that still works
without it, rather than fighting the kernel.</p>
<h2>TubeArchivist</h2>
<p>TubeArchivist self-hosts a searchable archive of downloaded YouTube content (Elasticsearch, Redis, and the TubeArchivist app itself). It's used to archive channels worth rewatching, including tutorials, long-form content, and anything that might disappear behind copyright strikes or channel deletions.</p>
<h2>Other Synology-specific quirks</h2>
<h3>Synology-specific troubleshooting</h3>
<p>Running Elasticsearch on Synology's DSM kernel isn't fully straightforward: newer Elasticsearch releases require a Linux kernel feature (SECCOMP) that Synology's kernel doesn't compile in. The fix is version-pinning Elasticsearch to the last release that still works without it, rather than fighting the kernel.</p>
<ul>
<li>Synology's Container Manager doesn't always auto-join containers to the same Docker network even when they're defined in one Compose file an explicit named network avoids silent connectivity failures between the app, Elasticsearch, and Redis.</li>
<li>Synology's Container Manager doesn't always auto-join containers to the same Docker network even when they're defined in one Compose file, so an explicit named network avoids silent connectivity failures between the app, Elasticsearch, and Redis.</li>
<li>A version upgrade of the app itself required a very specific step-by-step path (skipping versions breaks the database migration) rather than jumping straight to the latest release.</li>
<li>Initial indexing of large channels takes significant time: Elasticsearch needs to build and optimize indices for full-text search across video titles, descriptions, and metadata.</li>
</ul>
<h2>What's backed up</h2>
<h3>Playlist detection problems</h3>
<p>The main frustration with TubeArchivist is automatic playlist detection. New playlists added to subscribed channels are not always recognized by the app, meaning videos in those playlists get skipped during automatic downloads. The workaround is manually submitting individual videos or playlists through the TubeArchivist web interface using the built-in submission tool, which works but defeats the purpose of automated archiving. This issue remains unresolved.</p>
<h2>TubeSync</h2>
<p>TubeSync takes a simpler approach: it syncs YouTube playlists directly into a media server library (like Jellyfin or Plex) without the full archive UI. Everything is manual: playlists must be added by URL, and there's no automatic discovery of new content from subscribed channels. It's a pure download-and-organize tool rather than a searchable archive.</p>
<h2>Current state</h2>
<p>Both tools run in parallel on docker-host-nas, each with different strengths. TubeArchivist provides the better archive and search experience but has the playlist detection gap. TubeSync is more reliable for syncing specific playlists but requires manual curation. Neither has fully replaced the other yet, and the ideal solution of shutting one down hasn't been found.</p>
<h2>How it fits into the backup strategy</h2>
<p>Downloaded media files and Elasticsearch metadata are backed up as part of the NAS backup volumes, since they take significant time to re-download and re-index. The Redis cache is excluded, as it only holds session data and can be safely rebuilt on restart.</p>
<table>
<tr><th>Data</th><th>Included in NAS backup?</th></tr>
<tr><td>Downloaded media files</td><td>Yes, via the NAS's own backup volumes</td></tr>
<tr><td>Elasticsearch metadata/index</td><td>Yes, via the NAS's own backup volumes</td></tr>
<tr><td>Redis cache</td><td>No safe to delete/rebuild, only holds session data</td></tr>
<tr><td>Redis cache</td><td>No, safe to delete/rebuild, only holds session data</td></tr>
</table>
</div>
+7 -7
View File
@@ -26,11 +26,11 @@
<span class="site-title">{{ siteTitle | default(section + ".moonweb.org") }}</span>
</div>
<nav class="site-switcher">
<a href="https://hub.moonweb.org">hub</a>
<a href="https://infra.moonweb.org">infra</a>
<a href="https://smarthome.moonweb.org">smarthome</a>
<a href="https://code.moonweb.org">code</a>
<a href="https://retro.moonweb.org">retro</a>
<a href="https://www.moonweb.org/">hub</a>
<a href="https://www.moonweb.org/infra/">infra</a>
<a href="https://www.moonweb.org/smarthome/">smarthome</a>
<a href="https://www.moonweb.org/code/">code</a>
<a href="https://www.moonweb.org/retro/">retro</a>
<a href="https://stefankoelle.de">cv</a>
</nav>
</header>
@@ -41,8 +41,8 @@
</main>
<footer class="site-footer">
<p>Part of the <a href="https://hub.moonweb.org">moonweb.org</a> homelab.</p>
<p><a href="https://hub.moonweb.org/impressum/">Legal Notice &amp; Privacy Policy</a></p>
<p>Part of the <a href="https://www.moonweb.org/">moonweb.org</a> homelab.</p>
<p><a href="https://www.moonweb.org/impressum/">Legal Notice &amp; Privacy Policy</a></p>
</footer>
</body>
</html>
+1 -1
View File
@@ -43,7 +43,7 @@
"url": "{{ site.url }}",
"jobTitle": "Software Developer",
"sameAs": [
"https://hub.moonweb.org",
"https://www.moonweb.org/",
"https://www.linkedin.com/in/stefankoelle/",
"https://github.com/skoelle",
"https://www.xing.com/profile/Stefan_Koelle"
+24 -14
View File
@@ -22,23 +22,33 @@ layout: stefankoelle.njk
<div id="personalprojectscontent">
<h2>Personal Projects</h2>
<h3>moonweb.org (since 2020)</h3>
<p>My personal corner on the internet, documenting the homelab, smart home setups, infrastructure experiments, retro hardware, and the code that ties it all together. Everything runs on a homelab that has been evolving since the mid-90s.</p>
<ul>
<li><b><a href="https://www.moonweb.org/">hub.moonweb.org</a></b>: Overview of all sites and projects.</li>
<li><b><a href="https://www.moonweb.org/infra/">infra.moonweb.org</a></b>: Proxmox cluster, Synology NAS, Docker containers, networking, and backup strategies.</li>
<li><b><a href="https://www.moonweb.org/smarthome/">smarthome.moonweb.org</a></b>: Home Assistant, MQTT sensors, AirPlay audio, energy monitoring, and ESP32 dashboards.</li>
<li><b><a href="https://www.moonweb.org/code/">code.moonweb.org</a></b>: Curated catalog of public GitHub projects, sorted by topic.</li>
<li><b><a href="https://www.moonweb.org/retro/">retro.moonweb.org</a></b>: Atari ST, Amiga, DOS, MiSTer FPGA, and the retro computing corner.</li>
</ul>
<h3>Home Network Projects (2025-2026)</h3>
<ul>
<li><b>Docker Host (Proxmox PVE)</b><br>Virtualized Docker host with Debian 12.2 running containerized services including Portainer, Prometheus, Grafana, Gitea, monitoring exporters, and more. Full architecture documented on <a href="https://infra.moonweb.org/docker/">infra.moonweb.org</a>.</li>
<li><b>Docker Host (Proxmox PVE)</b><br>Virtualized Docker host with Debian 12.2 running containerized services including Portainer, Prometheus, Grafana, Gitea, monitoring exporters, and more. Full architecture documented on <a href="https://www.moonweb.org/infra//docker/">infra.moonweb.org</a>.</li>
<li><b>InfluxDB Time-Series Integration</b><br>Deployed InfluxDB 2.x for sensor data collection from Tasmota smart plugs and HomematicIP thermostats, replacing older MySQL-based storage with modern time-series architecture.</li>
<li><b>LED Matrix ESP32 InfluxDB Integration</b><br>Data persistence layer for <a href="/ledmatrix/">LED Matrix ESP32</a> sensor systems (DHT22 temperature/humidity) with InfluxDB 2.x backend. Automated collection at 60-second intervals for historical trend analysis.</li>
<li><b>Air Quality Dashboard</b><br>Dynamic HTML5 dashboard consuming real-time sensor data from InfluxDB via Flux query API. Displays temperature and humidity across all network locations, accessible via nginx reverse proxy.</li>
<li><b>Tasmota Energy Monitoring</b><br>Smart plug energy tracking with 8 Tasmota-enabled devices, integrating MQTT data collection and visualization. See also <a href="https://smarthome.moonweb.org/tasmota-energy/">smarthome.moonweb.org</a>.</li>
<li><b>Network Hardware & Monitoring</b><br>Four Zyxel GS1200-8HP managed switches in the server room, living room, and study, plus a <a href="https://infra.moonweb.org/openwrt/">Multi-WAN OpenWrt router</a> for failover and VLAN segmentation. All switches feed into <a href="https://infra.moonweb.org/monitoring/">Prometheus monitoring</a> via SNMP exporters, and IoT devices are isolated in VLAN 189.</li>
<li><b>Air Quality Dashboard</b><br>Dynamic HTML5 dashboard consuming real-time sensor data from InfluxDB via Flux query API. Displays temperature and humidity across all network locations, accessible via nginx reverse proxy. See also <a href="https://www.moonweb.org/smarthome//air-quality/">smarthome.moonweb.org</a>.</li>
<li><b>Tasmota Energy Monitoring</b><br>Smart plug energy tracking with 8 Tasmota-enabled devices, integrating MQTT data collection and visualization. See also <a href="https://www.moonweb.org/smarthome//tasmota-energy/">smarthome.moonweb.org</a>.</li>
<li><b>Network Hardware & Monitoring</b><br>Four Zyxel GS1200-8HP managed switches in the server room, living room, and study, plus a <a href="https://www.moonweb.org/infra//openwrt/">Multi-WAN OpenWrt router</a> for failover and VLAN segmentation. All switches feed into <a href="https://www.moonweb.org/infra//monitoring/">Prometheus monitoring</a> via SNMP exporters, and IoT devices are isolated in VLAN 189.</li>
</ul>
<h3>Retro Computing (2022/2023)</h3>
<ul>
<li>MiSTer and MiST FPGA Systems for preservation (see also <a href="https://retro.moonweb.org/mist-mister-fpga/">retro.moonweb.org</a>).</li>
<li>DOS System, Amiga System, and Atari ST System with the environment from the early 90s.</li>
<li>MiSTer and MiST FPGA Systems for preservation (see also <a href="https://www.moonweb.org/retro//mist-mister-fpga/">retro.moonweb.org</a>).</li>
<li><a href="https://www.moonweb.org/retro//dos-486-tower/">DOS System</a>, <a href="https://www.moonweb.org/retro//the-a1200/">Amiga System</a>, and <a href="https://www.moonweb.org/retro//atari-mega-st-fleet/">Atari ST System</a> with the environment from the early 90s.</li>
</ul>
<h3>Upgrade of old <a href="https://retro.moonweb.org/atari-mega-st-fleet/">Atari Mega 4</a> (2021)</h3>
<h3>Upgrade of old <a href="https://www.moonweb.org/retro//atari-mega-st-fleet/">Atari Mega 4</a> (2021)</h3>
<ul>
<li>Gotek Drive</li>
<li>More RAM</li>
@@ -57,10 +67,10 @@ layout: stefankoelle.njk
<h3>Various Docker Systems (since 2016)</h3>
<ul>
<li><b>Monitoring:</b> Prometheus, Grafana, InfluxDB, cAdvisor, Node Exporter, SNMP exporters</li>
<li><b>Smart Home:</b> Mosquitto MQTT broker, Home Assistant</li>
<li><b>Monitoring:</b> <a href="https://www.moonweb.org/infra//monitoring/">Prometheus, Grafana, InfluxDB</a>, cAdvisor, Node Exporter, SNMP exporters</li>
<li><b>Smart Home:</b> Mosquitto MQTT broker, <a href="https://www.moonweb.org/smarthome//homematic-mqtt/">Home Assistant</a></li>
<li><b>Infrastructure:</b> Portainer, Nginx Proxy Manager, Authelia (SSO), Watchtower, Healthchecks</li>
<li><b>Applications:</b> Gitea (GitHub mirror), Stirling PDF, TubeArchivist, TubeSync, SearXNG</li>
<li><b>Applications:</b> Gitea (GitHub mirror), <a href="https://www.moonweb.org/infra//docker/">Stirling PDF</a>, <a href="https://www.moonweb.org/smarthome//tubearchivist/">TubeArchivist</a>, <a href="https://www.moonweb.org/smarthome//tubearchivist/">TubeSync</a>, <a href="https://www.moonweb.org/infra//docker/">SearXNG</a></li>
</ul>
<h3>Walk-on Song (2019)</h3>
@@ -70,7 +80,7 @@ layout: stefankoelle.njk
<li>MP3 Module for playback on attached speaker.</li>
</ul>
<h3>Kids RFID MP3 Player (2018)</h3>
<h3><a href="https://www.moonweb.org/smarthome//kids-rfid-player/">Kids RFID MP3 Player</a> (2018)</h3>
<ul>
<li>Interactive audio playback system utilizing RFID technology.</li>
<li>Powered by an Arduino Nano for compact and efficient operation.</li>
@@ -105,7 +115,7 @@ layout: stefankoelle.njk
<h3>Mailbox/Fidonet (1994 - 2000)</h3>
<ul>
<li>Operated a BBS and Fidonet Node (see also <a href="https://retro.moonweb.org/atari-networking-bbs/">Networking &amp; BBS</a> on retro.moonweb.org).</li>
<li>Operated a BBS and Fidonet Node (see also <a href="https://28k8.moonweb.org/bbs/fido/nodelist/">FidoNet Nodelist</a> on 28k8.moonweb.org).</li>
<li>Managed a node in Augsburg with an uplink directly to the NC in Munich.</li>
<li>Software was running on an OS/2 computer.</li>
<li>Utilized 2x ISDN and 1x MODEM connections.</li>
@@ -136,7 +146,7 @@ layout: stefankoelle.njk
<h3>AI-Assisted Development</h3>
<p>I've fully embraced the AI-assisted development paradigm since early 2025. At work, I've taught <strong>Claude Code</strong> a workflow for handling Jira stories end-to-end: it evaluates requirements, provides feedback to the PO, and creates a detailed implementation plan. From there, it's mostly about supervising the plan while the code generation runs largely autonomously. For code review, we use a second AI (e.g., Codex) as a quality gate to catch issues that might slip through. In migration projects, I define the exact plan upfront and Claude Code executes it with increasing independence. The output gain is massive, delivering in a fraction of the time.</p>
<p>Privately, I work with <strong><a href="https://opencode.ai">OpenCode</a></strong> paired with OpenCode Zen, Merge Gateway, and OpenRouter, mostly for Python-based home lab and automation projects. See my <a href="https://code.moonweb.org">GitHub projects</a> for examples and the <a href="https://infra.moonweb.org/dev-environment/">Dev VM setup</a> for the infrastructure behind this workflow.</p>
<p>Privately, I work with <strong><a href="https://opencode.ai">OpenCode</a></strong> paired with OpenCode Zen, Merge Gateway, and OpenRouter, mostly for Python-based home lab and automation projects. See my <a href="https://www.moonweb.org/code/">GitHub projects</a> for examples and the <a href="https://www.moonweb.org/infra//dev-environment/">Dev VM setup</a> for the infrastructure behind this workflow.</p>
<h3>.NET Core</h3>
<ul>
@@ -233,5 +243,5 @@ layout: stefankoelle.njk
<p>Stefan Kölle<br>Neumarkter Str. 86c<br>81673 München</p>
<p>Phone/Fax: +49-(89)-20006547<br>E-Mail: <a id="email-impressum" href="#">[email protected]</a></p>
<p>Responsible for content according to § 18 (2) MStV: Stefan Kölle, address as above.</p>
<p>Full legal notice & privacy policy for this website and the moonweb.org network: <a href="https://hub.moonweb.org/impressum">hub.moonweb.org/impressum</a>
<p>Full legal notice & privacy policy for this website and the moonweb.org network: <a href="https://www.moonweb.org//impressum/">hub.moonweb.org/impressum</a>
</div>
+3 -3
View File
@@ -134,14 +134,14 @@ description: "WiFi-controlled LED display solution with REST API, DHT22 sensor,
<div class="feature-icon">📊</div>
<div class="feature-text">
<h3>Sensor Readings</h3>
<p>Live temperature and humidity from the on-board DHT22 sensor, stored in <a href="https://smarthome.moonweb.org/homematic-mqtt/">InfluxDB via MQTT</a></p>
<p>Live temperature and humidity from the on-board DHT22 sensor, stored in <a href="https://www.moonweb.org/smarthome//homematic-mqtt/">InfluxDB via MQTT</a></p>
</div>
</div>
<div class="feature">
<div class="feature-icon">🔔</div>
<div class="feature-text">
<h3>Status Messages</h3>
<p>Notifications like "washing machine done" triggered via <a href="https://smarthome.moonweb.org/tasmota-energy/">MQTT and Tasmota energy monitoring</a></p>
<p>Notifications like "washing machine done" triggered via <a href="https://www.moonweb.org/smarthome//tasmota-energy/">MQTT and Tasmota energy monitoring</a></p>
</div>
</div>
<div class="feature">
@@ -575,7 +575,7 @@ ESP32 will be restarted (with display shutdown)</pre>
</div>
<div class="footer-bottom">
<p>&copy; 2026 <a href="https://stefankoelle.de/">Stefan Kölle</a>. LED Matrix WebServer v1.08.</p>
<p><a href="https://hub.moonweb.org/impressum/">Legal Notice &amp; Privacy Policy</a></p>
<p><a href="https://www.moonweb.org//impressum/">Legal Notice &amp; Privacy Policy</a></p>
</div>
</footer>
+6
View File
@@ -17,6 +17,12 @@ echo "==> Installing WeasyPrint..."
pip install -q -r "$REQUIREMENTS"
echo "==> Building stefankoelle with Eleventy..."
# Temporarily hide .eleventyignore so the standalone stefankoelle build works
IGNORE_FILE="$PROJECT_DIR/.eleventyignore"
if [ -f "$IGNORE_FILE" ]; then
mv "$IGNORE_FILE" "$IGNORE_FILE.bak"
trap 'mv "$IGNORE_FILE.bak" "$IGNORE_FILE"' EXIT
fi
npx @11ty/eleventy --config="$PROJECT_DIR/stefankoelle/eleventy.config.js"
echo "==> Generating PDF..."
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
const markdownIt = require("markdown-it");
const md = new markdownIt({ html: true, breaks: true });
module.exports = function (eleventyConfig) {
eleventyConfig.addPassthroughCopy({ "src/assets": "/timecapsule/assets" });
eleventyConfig.addPassthroughCopy({ "src/.htaccess": "/timecapsule/.htaccess" });
eleventyConfig.setLibrary("md", md);
eleventyConfig.addFilter("toIsoDate", function(dateStr) {
var parts = String(dateStr).split("/");
if (parts.length === 3) {
var m = parts[0].padStart(2, "0");
var d = parts[1].padStart(2, "0");
var y = parts[2].padStart(4, "0");
return y + "-" + m + "-" + d;
}
return dateStr;
});
eleventyConfig.addPairedShortcode("pseudonymBox", function (content, title) {
const body = md.render(content || "");
return `<p align="right">
<table border="0" cellspacing="1" cellpadding="3" width="350" bgcolor="#555544" class="pseudonym-box">
<tr><td><font color="#CCAA66"><b>${title}</b></font></td></tr>
<tr><td bgcolor="#FFFFFF">${body}</td></tr>
</table>
</p>`;
});
return {
dir: { input: "src", includes: "_includes", data: "_data", output: "_site" },
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk",
dataTemplateEngine: "njk",
};
};
+2505
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"name": "moonweb-timecapsule",
"version": "1.0.0",
"private": true,
"description": "Timecapsule: 2000s retro moonweb.org website",
"scripts": {
"dev": "npx @11ty/eleventy --serve --port=8087",
"build": "npx @11ty/eleventy"
},
"dependencies": {
"@11ty/eleventy": "^2.0.1",
"markdown-it": "^14.1.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
RewriteEngine On
RewriteBase /
ErrorDocument 404 /404/index.html
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>moonweb.org - Page Not Found</title>
<meta name="description" content="The page you are looking for does not exist on moonweb.org.">
<meta property="og:title" content="moonweb.org - Page Not Found">
<meta property="og:description" content="The page you are looking for does not exist on moonweb.org.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://www.moonweb.org/timecapsule/404/">
<meta property="og:site_name" content="moonweb.org">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="moonweb.org - Page Not Found">
<meta name="twitter:description" content="The page you are looking for does not exist on moonweb.org.">
<link rel="stylesheet" type="text/css" href="/timecapsule/assets/css/moonweb.css">
<link rel="icon" href="/timecapsule/assets/images/favicon.ico">
</head>
<body background="/timecapsule/assets/images/background.gif" topmargin="0" leftmargin="0" marginwidth="0" marginheight="0">
<div class="mobile-header">
<img src="/timecapsule/assets/images/logomobile.gif" alt="M O O N W E B" class="mobile-logo">
</div>
<table border="0" cellpadding="0" cellspacing="0" width="750" height="100%" class="layout-main"><tr><td valign="top" class="sidebar-left">
<table border="0" cellpadding="0" cellspacing="0" width="108">
<tr><td>
<img src="/timecapsule/assets/pageImage/beginning.jpg" alt="beginning" height="162" width="108" border="0">
</td></tr>
</table>
</td><td valign="top" class="content-area">
<table border="0" cellpadding="7" cellspacing="0" width="492"><tr><td>
<h1>Page Not Found</h1>
<p>The page you are looking for does not exist on moonweb.org.</p>
<p>It may have been moved, renamed, or removed.</p>
<p><a href="/timecapsule/">Go to the beginning</a></p>
</td></tr></table>
</td><td valign="top" class="sidebar-right">
<table border="0" cellpadding="0" cellspacing="0" width="150">
<tr height="20"><td>&nbsp;</td></tr>
<tr height="295"><td>
<table border="0" cellpadding="0" cellspacing="0"><tr>
<td width="130" valign="top">
</td>
<td width="60"><img src="/timecapsule/assets/images/logo.gif" height="295" width="61" alt="M O O N W E B"></td>
</tr></table>
</td></tr>
</table>
</td></tr>
<tr><td align="center"></td><td colspan="2">
<table border="0" cellpadding="10" cellspacing="0" width="610"><tr><td>
<hr>
<p class="footer">
<a href="/timecapsule/legal-notice/">Legal Notice</a> - (c) 2001-2026 moonweb.org
</p>
</td></tr></table>
</td></tr>
</table>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
[
[
"beginning"
],
[
"projects",
"products",
"acoustics"
],
[
"location",
"background",
"partners"
],
[
"contact"
]
]

Some files were not shown because too many files have changed in this diff Show More