mirror of
https://github.com/skoelle/moonweb-site.git
synced 2026-09-18 01:10:25 +00:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c736532e1 | ||
|
|
401868cde5 | ||
|
|
dbb791b1a6 | ||
|
|
c77ad91e0e | ||
|
|
10b05736c8 | ||
|
|
84cd3f8562 | ||
|
|
9cda842250 | ||
|
|
37f7d8f1fb | ||
|
|
c76de3ec36 | ||
|
|
7524210768 | ||
|
|
41a9236ca7 | ||
|
|
7c3e29d282 | ||
|
|
2b997c11d0 | ||
|
|
466df3bd04 | ||
|
|
a97c3d3a89 | ||
|
|
d213154b79 | ||
|
|
13fdffce07 | ||
|
|
b1639bbece | ||
|
|
156595cbe4 | ||
|
|
0396b755f0 | ||
|
|
1ea2967b35 | ||
|
|
dacbd57302 | ||
|
|
7fc47113bb | ||
|
|
5cd19867b7 | ||
|
|
e9e492abc0 | ||
|
|
2fdb631e60 | ||
|
|
6e1b892848 |
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -15,3 +15,6 @@ dist/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
timecapsule/_site/
|
||||||
|
timecapsule/node_modules/
|
||||||
|
.tmp/
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
# AGENTS.md — moonweb-site
|
# 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 |
|
| Site | URL | Zweck | Status |
|
||||||
|------|--------|-------|--------|
|
|------|-----|-------|--------|
|
||||||
| hub | hub.moonweb.org | Zentrale Indexseite, verlinkt alles | Fertig |
|
| hub | www.moonweb.org/ | Zentrale Indexseite, verlinkt alles | Fertig |
|
||||||
| infra | infra.moonweb.org | Infrastruktur-Übersicht (Proxmox, Synology, Netzwerk) | Übersicht + 3 Detailseiten |
|
| infra | www.moonweb.org/infra/ | Infrastruktur-Uebersicht (Proxmox, Synology, Netzwerk) | Uebersicht + 8 Detailseiten |
|
||||||
| smarthome | smarthome.moonweb.org | Smart Home Projekte und Dashboards | Übersicht + 6 Detailseiten |
|
| smarthome | www.moonweb.org/smarthome/ | Smart Home Projekte und Dashboards | Uebersicht + 9 Detailseiten |
|
||||||
| code | code.moonweb.org | GitHub-Projekte (aggregiert via .moonweb.yml) | Fertig (14 Repos) |
|
| code | www.moonweb.org/code/ | GitHub-Projekte (aggregiert via .moonweb.yml) | Fertig |
|
||||||
| retro | retro.moonweb.org | Physische Retro-Hardware | Nur Übersicht (WIP) |
|
| 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)
|
### 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)
|
### Andere Sites (nicht im Monorepo)
|
||||||
|
|
||||||
- www.moonweb.org — 2000er Internet-Zeitkapsel
|
|
||||||
- 28k8.moonweb.org — 90er BBS/Scene-Archiv
|
- 28k8.moonweb.org — 90er BBS/Scene-Archiv
|
||||||
|
- buildbroken.moonweb.org — .NET Open Space Blog Archiv
|
||||||
|
|
||||||
## Dateistruktur
|
## Dateistruktur
|
||||||
|
|
||||||
```
|
```
|
||||||
moonweb-site/
|
moonweb-site/
|
||||||
├── hub/ # Eleventy-Config + index.njk
|
├── hub/ # Eleventy-Config + index.njk
|
||||||
├── infra/ # Eleventy-Config + index.njk + 3 Subseiten
|
├── infra/ # Eleventy-Config + index.njk + 8 Subseiten
|
||||||
├── smarthome/ # Eleventy-Config + index.njk + 6 Subseiten
|
├── smarthome/ # Eleventy-Config + index.njk + 9 Subseiten
|
||||||
├── code/ # Eleventy-Config + index.njk
|
├── code/ # Eleventy-Config + index.njk
|
||||||
│ └── _data/repos.json
|
│ └── _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
|
├── stefankoelle/ # Eleventy-Config + Onepager
|
||||||
│ ├── eleventy.config.js
|
│ ├── eleventy.config.js
|
||||||
│ ├── _includes/
|
│ ├── _includes/
|
||||||
@@ -52,10 +57,11 @@ moonweb-site/
|
|||||||
│ ├── base.css
|
│ ├── base.css
|
||||||
│ └── theme-*.css
|
│ └── theme-*.css
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── github-aggregator/
|
│ ├── github-aggregator/
|
||||||
|
│ └── merge-moonweb.sh # Merge-Skript fuer Deployment
|
||||||
├── .github/workflows/
|
├── .github/workflows/
|
||||||
│ ├── build-deploy.yml # CI/CD: Cloudflare Pages (5 Sites)
|
│ ├── build-deploy-moonweb.yml # CI/CD: IONOS SFTP (www.moonweb.org)
|
||||||
│ └── deploy-stefankoelle.yml # CI/CD: IONOS SFTP (stefankoelle.de)
|
│ └── deploy-stefankoelle.yml # CI/CD: IONOS SFTP (stefankoelle.de)
|
||||||
├── DESIGN.md
|
├── DESIGN.md
|
||||||
├── SPEC.md
|
├── SPEC.md
|
||||||
├── PLAN.md
|
├── PLAN.md
|
||||||
@@ -66,10 +72,11 @@ moonweb-site/
|
|||||||
|
|
||||||
## Technischer Stack
|
## 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)
|
- **Templates:** Nunjucks (.njk)
|
||||||
- **CSS:** Variables-basiert mit Accent-Farben pro Site
|
- **CSS:** Variables-basiert mit Accent-Farben pro Site
|
||||||
- **Deploy (moonweb):** Cloudflare Pages (5 separate Projects)
|
- **Deploy (moonweb):** IONOS SFTP
|
||||||
- **Deploy (stefankoelle):** IONOS SFTP
|
- **Deploy (stefankoelle):** IONOS SFTP
|
||||||
- **CI/CD:** GitHub Actions (2 Workflows)
|
- **CI/CD:** GitHub Actions (2 Workflows)
|
||||||
|
|
||||||
@@ -77,32 +84,54 @@ moonweb-site/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run prebuild # Pre-Build Tasks (CV PDF generieren)
|
cd timecapsule && npm install # Timecapsule Dependencies
|
||||||
npm run dev:hub # localhost:8081
|
npm run prebuild # Pre-Build Tasks (CV PDF generieren)
|
||||||
npm run dev:infra # localhost:8082
|
npm run dev:hub # localhost:8081
|
||||||
npm run dev:smarthome # localhost:8083
|
npm run dev:infra # localhost:8082
|
||||||
npm run dev:code # localhost:8084
|
npm run dev:smarthome # localhost:8083
|
||||||
npm run dev:retro # localhost:8085
|
npm run dev:code # localhost:8084
|
||||||
npm run dev:stefankoelle # localhost:8086
|
npm run dev:retro # localhost:8085
|
||||||
npm run build # Alle Sites bauen
|
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
|
## Design-Prinzipien
|
||||||
|
|
||||||
1. **Header konsistent** — Identischer Site-Switcher auf allen Home-Sites
|
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
|
3. **Accent-Farben:** hub=#3b6ea5, infra=#99333A, smarthome=#1f8a8a, code=#3E5098, retro=#8a6d3b
|
||||||
4. **Englisch** — Alle Sites komplett auf Englisch
|
4. **Englisch** — Alle Sites komplett auf Englisch
|
||||||
5. **Keine Analytics** — Keine Tracking-Tools
|
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
|
## stefankoelle.de
|
||||||
|
|
||||||
- Eigene Eleventy-Config (nicht shared base.njk)
|
- Eigene Eleventy-Config (nicht shared base.njk)
|
||||||
- Eigenes CSS-Design (nicht moonweb design system)
|
- Eigenes CSS-Design (nicht moonweb design system)
|
||||||
- Onepager mit Anchor-Links (bleibt so)
|
- Onepager mit Anchor-Links (bleibt so)
|
||||||
- Deploy via SFTP auf IONOS `/deploy/stefankoelle/`
|
- Deploy via SFTP auf IONOS `/websites/stefankoelle/`
|
||||||
- CV-Partial zentral pflegbar (einmal aendern → Web + PDF aktualisieren)
|
- CV-Partial zentral pflegbar (einmal aendern -> Web + PDF aktualisieren)
|
||||||
- LED Matrix als eigene Seite unter stefankoelle.de/ledmatrix/
|
- LED Matrix als eigene Seite unter stefankoelle.de/ledmatrix/
|
||||||
|
|
||||||
### CV PDF Generierung
|
### CV PDF Generierung
|
||||||
@@ -116,7 +145,7 @@ npm run prebuild # Alle Pre-Build Tasks (inkl. CV PDF)
|
|||||||
|
|
||||||
Dateien:
|
Dateien:
|
||||||
- `stefankoelle/pdf/cv-style.css` — WeasyPrint-Stylesheet (A4, Typografie)
|
- `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/cv-print.njk` — Standalone HTML-Template (nur CV-Content)
|
||||||
- `stefankoelle/pdf/cv.pdf` — Generiertes PDF (Output)
|
- `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 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/index.njk`: `<link rel="stylesheet" href="/assets/style.css?v=N">`
|
||||||
- `stefankoelle/ledmatrix/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
|
## CI/CD
|
||||||
|
|
||||||
### Cloudflare Pages (moonweb)
|
### IONOS SFTP (www.moonweb.org)
|
||||||
`.github/workflows/build-deploy.yml` baut hub, infra, smarthome, code, retro.
|
`.github/workflows/build-deploy-moonweb.yml` baut alle moonweb Sites (hub, infra, smarthome, code, retro, timecapsule) und deployed per SFTP.
|
||||||
|
|
||||||
Benötigte Secrets:
|
Benötigte Secrets:
|
||||||
- `CLOUDFLARE_API_TOKEN`
|
- `IONOS_SFTP_HOST`
|
||||||
- `CLOUDFLARE_ACCOUNT_ID`
|
- `IONOS_SFTP_USER`
|
||||||
|
- `IONOS_SFTP_PASSWORD`
|
||||||
|
|
||||||
### IONOS SFTP (stefankoelle.de)
|
### IONOS SFTP (stefankoelle.de)
|
||||||
`.github/workflows/deploy-stefankoelle.yml` baut stefankoelle.de und deployed per SFTP.
|
`.github/workflows/deploy-stefankoelle.yml` baut stefankoelle.de und deployed per SFTP.
|
||||||
@@ -150,9 +180,11 @@ Benötigte Secrets:
|
|||||||
|
|
||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
|
|
||||||
- retro/ ist bewusst rudimentär gehalten
|
- retro/ ist bewusst rudimentaer gehalten
|
||||||
- Querverlinkungen stefankoelle.de <-> smarthome (zukuenftig)
|
- Querverlinkungen stefankoelle.de <-> smarthome (zukuenftig)
|
||||||
- Ledmatrix ggf. nach smarthome verschieben (when ready)
|
- Ledmatrix ggf. nach smarthome verschieben (when ready)
|
||||||
|
- Cloudflare Redirects einrichten (nach Deploy)
|
||||||
|
- Google Search Console: Neue Property www.moonweb.org
|
||||||
|
|
||||||
## GitHub Aggregator
|
## GitHub Aggregator
|
||||||
|
|
||||||
@@ -191,10 +223,10 @@ python3 -m venv .venv
|
|||||||
|
|
||||||
## Text / Stil
|
## Text / Stil
|
||||||
|
|
||||||
- Keine Em-Dashes (—) verwenden, stattdessen umformulieren (Komma, Satzzeichen, neu formulieren)
|
- Keine Em-Dashes verwenden, stattdessen umformulieren (Komma, Satzzeichen, neu formulieren)
|
||||||
|
|
||||||
## Python / pip
|
## Python / pip
|
||||||
|
|
||||||
- Niemals `pip install` direkt ausführen
|
- Niemals `pip install` direkt ausfuehren
|
||||||
- Immer ein virtuelles Umfeld (`.venv`) anlegen und darin arbeiten
|
- Immer ein virtuelles Umfeld (`.venv`) anlegen und darin arbeiten
|
||||||
- `python3 -m venv .venv` im Projektverzeichnis
|
- `python3 -m venv .venv` im Projektverzeichnis
|
||||||
|
|||||||
@@ -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
|
www.moonweb.org/ -> Central index & gateway
|
||||||
infra.moonweb.org → 🏗️ Infrastructure overview (Proxmox, Synology, Docker)
|
www.moonweb.org/infra/ -> Infrastructure overview (Proxmox, Synology, Docker)
|
||||||
smarthome.moonweb.org → 🏡 Smart home projects & dashboards
|
www.moonweb.org/smarthome/ -> Smart home projects & dashboards
|
||||||
code.moonweb.org → 💻 Curated GitHub project catalog
|
www.moonweb.org/code/ -> Curated GitHub project catalog
|
||||||
retro.moonweb.org → 🕹️ Physical retro hardware collection
|
www.moonweb.org/retro/ -> Physical retro hardware collection
|
||||||
stefankoelle.de → 👤 CV, career, personal site (LED Matrix docs)
|
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 │
|
| GitHub Actions CI |
|
||||||
│ ┌───────────┐ ┌────────────┐ ┌─────────────┐ ┌─────────┐ │
|
| +-----------+ +------------+ +-------------+ +---------+ +------+ |
|
||||||
│ │ build:hub │ │build:infra │ │ build:smart │ │ build:… │ │
|
| | build:hub | |build:infra | | build:smart | | build:... | | build:timecapsule | |
|
||||||
│ └─────┬─────┘ └─────┬──────┘ └──────┬──────┘ └────┬────┘ │
|
| +-----+-----+ +-----+------+ +------+------+ +----+----+ +----+----+ +--------+ |
|
||||||
│ │ │ │ │ │
|
| | | | | | | |
|
||||||
│ ▼ ▼ ▼ ▼ │
|
| v v v v v v |
|
||||||
│ dist/hub/ dist/infra/ dist/smarthome/ dist/…/ │
|
| dist/hub/ dist/infra/ dist/smarthome/ dist/.../ dist/timecapsule/ |
|
||||||
└────────┬───────────────────────────────────────────┬────────┘
|
+--------+---------------------------------------------------+--------------------+
|
||||||
│ │
|
| |
|
||||||
▼ ▼
|
v v
|
||||||
┌──────────────────┐ ┌─────────────────┐
|
+------------------+ +------------------+
|
||||||
│ Cloudflare Pages │ │ IONOS SFTP │
|
| IONOS SFTP | | IONOS SFTP |
|
||||||
│ (5 sites) │ │ (stefankoelle) │
|
| (www.moonweb.org)| | (stefankoelle.de)|
|
||||||
└────────┬─────────┘ └────────┬────────┘
|
+--------+---------+ +--------+---------+
|
||||||
▼ ▼
|
v v
|
||||||
hub / infra / ... stefankoelle.de
|
www.moonweb.org/* stefankoelle.de
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
| Layer | Technology | Why |
|
| 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 |
|
| **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 |
|
| **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 |
|
| **CI/CD** | [GitHub Actions](https://github.com/features/actions) | Matrix build for all sites, artifact upload, merge step, parallel deploy |
|
||||||
| **Deploy** | [Cloudflare Workers](https://workers.cloudflare.com/) | Static asset hosting via `wrangler pages deploy`, one worker per site |
|
| **Deploy** | IONOS SFTP | Static hosting via SFTP upload |
|
||||||
| **DNS** | Cloudflare | Already managing DNS — zero additional setup for Pages custom domains |
|
| **DNS** | Cloudflare | DNS management + redirects from old subdomains |
|
||||||
| **GitHub Catalog** | Python aggregator | Reads `.moonweb.yml` from each repo, outputs `repos.json` |
|
| **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
|
## Local Development
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install # install Eleventy + deps
|
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:hub # http://localhost:8081
|
||||||
npm run dev:infra # http://localhost:8082
|
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:code # http://localhost:8084
|
||||||
npm run dev:retro # http://localhost:8085
|
npm run dev:retro # http://localhost:8085
|
||||||
npm run dev:stefankoelle # http://localhost:8086
|
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.
|
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
|
```bash
|
||||||
npm run prebuild # pre-build tasks (CV PDF)
|
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 build:hub # build single site
|
||||||
|
npm run merge:moonweb # merge all moonweb sites into dist/ for deployment
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
moonweb-site/
|
moonweb-site/
|
||||||
├── hub/ # 🏠 Central index & gateway
|
├── hub/ # Central index & gateway
|
||||||
├── infra/ # 🏗️ Infra overview + 3 detail pages
|
├── infra/ # Infra overview + 8 detail pages
|
||||||
│ ├── backup-strategy/
|
│ ├── backup-strategy/
|
||||||
│ ├── monitoring/
|
│ ├── monitoring/
|
||||||
│ └── dev-environment/
|
│ ├── dev-environment/
|
||||||
├── smarthome/ # 🏡 Smart home overview + 6 detail pages
|
│ └── ...
|
||||||
|
├── smarthome/ # Smart home overview + 9 detail pages
|
||||||
│ ├── homematic-mqtt/
|
│ ├── homematic-mqtt/
|
||||||
│ ├── tasmota-energy/
|
│ ├── tasmota-energy/
|
||||||
│ ├── balkonpi/
|
│ └── ...
|
||||||
│ ├── airplay-audio/
|
├── code/ # GitHub catalog
|
||||||
│ ├── octoprint/
|
|
||||||
│ └── tubearchivist/
|
|
||||||
├── code/ # 💻 GitHub catalog
|
|
||||||
│ └── _data/repos.json # populated by the aggregator
|
│ └── _data/repos.json # populated by the aggregator
|
||||||
├── retro/ # 🕹️ Retro hardware (WIP)
|
├── retro/ # Retro hardware + 13 detail pages
|
||||||
├── stefankoelle/ # 👤 CV, career, personal site
|
├── timecapsule/ # 2000s retro design (Eleventy 2.x)
|
||||||
|
│ ├── eleventy.config.js
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── src/
|
||||||
|
├── stefankoelle/ # CV, career, personal site
|
||||||
│ ├── eleventy.config.js
|
│ ├── eleventy.config.js
|
||||||
│ ├── index.njk # Onepager (CV, Projects, Languages)
|
│ ├── index.njk # Onepager (CV, Projects, Languages)
|
||||||
│ ├── cv-print.njk # CV-only for PDF generation
|
│ ├── cv-print.njk # CV-only for PDF generation
|
||||||
│ ├── ledmatrix/ # LED Matrix WebServer documentation
|
│ ├── ledmatrix/ # LED Matrix WebServer documentation
|
||||||
│ └── assets/ # CSS, JS, images, favicons
|
│ └── assets/ # CSS, JS, images, favicons
|
||||||
├── shared/ # 🔧 Shared components
|
├── shared/ # Shared components
|
||||||
│ ├── _includes/
|
│ ├── _includes/
|
||||||
│ │ ├── base.njk # base layout (header, site-switcher, footer)
|
│ │ ├── base.njk # base layout (header, site-switcher, footer)
|
||||||
│ │ └── card-grid.njk # card-grid template with emoji support
|
│ │ └── card-grid.njk # card-grid template with emoji support
|
||||||
│ ├── base.css # shared CSS (layout, cards, typography)
|
│ ├── base.css # shared CSS (layout, cards, typography)
|
||||||
│ └── theme-*.css # accent colors per domain
|
│ └── theme-*.css # accent colors per domain
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── github-aggregator/ # 🐍 Python: reads .moonweb.yml → repos.json
|
│ ├── github-aggregator/ # Python: reads .moonweb.yml -> repos.json
|
||||||
│ ├── aggregate.py
|
│ └── merge-moonweb.sh # Merge script for deployment
|
||||||
│ ├── example.moonweb.yml
|
|
||||||
│ └── README.md
|
|
||||||
├── .github/workflows/
|
├── .github/workflows/
|
||||||
│ └── build-deploy.yml # ⚙️ CI/CD: build + deploy to Cloudflare
|
│ ├── build-deploy-moonweb.yml # CI/CD: build + deploy to IONOS SFTP
|
||||||
├── DESIGN.md # 📋 Initial concept (German)
|
│ └── deploy-stefankoelle.yml # CI/CD: stefankoelle.de to IONOS SFTP
|
||||||
├── SPEC.md # 📋 Full specification (English, 159 lines)
|
├── DESIGN.md # Initial concept
|
||||||
├── PLAN.md # 📋 Implementation plan
|
├── SPEC.md # Full specification
|
||||||
├── TODO.md # 📋 Open items & workflow
|
├── PLAN.md # Implementation plan
|
||||||
|
├── TODO.md # Open items & workflow
|
||||||
└── package.json # npm scripts for dev/build
|
└── 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
|
push to main
|
||||||
│
|
|
|
||||||
├── Build (matrix: hub, infra, smarthome, code, retro)
|
+-- Build (matrix: hub, infra, smarthome, code, retro)
|
||||||
│ ├── checkout → setup-node (22) → npm ci
|
| +-- checkout -> setup-node (22) -> npm ci
|
||||||
│ ├── npm run build:<site>
|
| +-- npm run build:<site>
|
||||||
│ ├── validate dist/<site>/ exists & non-empty
|
| +-- validate dist/<site>/ exists & non-empty
|
||||||
│ └── upload artifact (7-day retention)
|
| +-- upload artifact (7-day retention)
|
||||||
│
|
|
|
||||||
└── Deploy (matrix: 5 Cloudflare Workers)
|
+-- Build timecapsule
|
||||||
├── download artifact
|
| +-- cd timecapsule && npm ci
|
||||||
├── generate wrangler.toml
|
| +-- npm run build:timecapsule
|
||||||
└── wrangler pages deploy
|
| +-- 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:**
|
**Required secrets:**
|
||||||
- `CLOUDFLARE_API_TOKEN` — Workers:Edit permission
|
- `IONOS_SFTP_HOST`
|
||||||
- `CLOUDFLARE_ACCOUNT_ID` — Cloudflare account ID
|
- `IONOS_SFTP_USER`
|
||||||
|
- `IONOS_SFTP_PASSWORD`
|
||||||
|
|
||||||
### IONOS SFTP (stefankoelle.de)
|
### IONOS SFTP (stefankoelle.de)
|
||||||
|
|
||||||
@@ -184,12 +177,12 @@ Defined in `.github/workflows/deploy-stefankoelle.yml`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
push to main (paths: stefankoelle/**)
|
push to main (paths: stefankoelle/**)
|
||||||
│
|
|
|
||||||
├── Build stefankoelle
|
+-- Build stefankoelle
|
||||||
│ └── npm run build:stefankoelle
|
| +-- npm run build:stefankoelle
|
||||||
│
|
|
|
||||||
└── Deploy via SFTP
|
+-- Deploy via SFTP
|
||||||
└── lftp mirror → IONOS /deploy/stefankoelle/
|
+-- SFTP upload to IONOS /websites/stefankoelle/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Required secrets:**
|
**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:
|
`scripts/github-aggregator/aggregate.py` automatically builds the project catalog:
|
||||||
|
|
||||||
@@ -214,7 +230,7 @@ push to main (paths: stefankoelle/**)
|
|||||||
```yaml
|
```yaml
|
||||||
title: "MVG Departures"
|
title: "MVG Departures"
|
||||||
category: code # code | smarthome | infra
|
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
|
status: active
|
||||||
stack: [Python, FastAPI]
|
stack: [Python, FastAPI]
|
||||||
hosted_on: "Docker Host Debian (PVE)"
|
hosted_on: "Docker Host Debian (PVE)"
|
||||||
@@ -229,42 +245,41 @@ export GITHUB_TOKEN=ghp_xxx
|
|||||||
python scripts/github-aggregator/aggregate.py
|
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 |
|
| Site | Detail pages? | Rule |
|
||||||
|------|--------------|------|
|
|------|--------------|------|
|
||||||
| smarthome | ✅ Yes | When enough content exists — no placeholder cards |
|
| smarthome | Yes | When enough content exists - no placeholder cards |
|
||||||
| infra | ⚠️ Rarely | Deliberately shallow — sensitive data (IPs, keys, passwords) stripped |
|
| infra | Rarely | Deliberately shallow - sensitive data (IPs, keys, passwords) stripped |
|
||||||
| code | ❌ Never | Overview cards + GitHub links only — no README duplication |
|
| code | Never | Overview cards + GitHub links only - no README duplication |
|
||||||
| retro | 🔨 Minimal | Still WIP — honest minimal overview, no over-investment |
|
| 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.
|
**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 |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `DESIGN.md` | Initial concept and design decisions (German) |
|
| `DESIGN.md` | Initial concept and design decisions |
|
||||||
| `SPEC.md` | Complete specification — what gets built (English) |
|
| `SPEC.md` | Complete specification - what gets built |
|
||||||
| `PLAN.md` | Phased implementation plan |
|
| `PLAN.md` | Phased implementation plan |
|
||||||
| `TODO.md` | Open items, workflow, and current status |
|
| `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
|
||||||
|
|
||||||
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
**URL-Struktur nach Migration:**
|
||||||
|-------|--------|-----------|
|
| URL | Inhalt |
|
||||||
| 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 |
|
| `www.moonweb.org/` | Hub (neue Root) |
|
||||||
| Phase 2 — CI/CD | ✅ fertig | build-deploy.yml mit Caching + Validation + Summary |
|
| `www.moonweb.org/infra/` | Infra |
|
||||||
| Phase 3 — Launch | ✅ fertig | Cloudflare Pages live, Smoke-Test bestanden |
|
| `www.moonweb.org/smarthome/` | Smarthome |
|
||||||
| Phase 4 — Deferred | ⏸️ zurückgestellt | 28k8, Analytics |
|
| `www.moonweb.org/code/` | Code |
|
||||||
| Phase 5 — stefankoelle.de | ✅ fertig | Ins Monorepo migriert, Eleventy-Build, SFTP-Deployment |
|
| `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
|
- [x] 1.1 Dateien aus ../moonweb-www/src/ nach timecapsule/src/ kopieren
|
||||||
- [ ] Perplexity-Rückkanal
|
- [x] 1.2 timecapsule/eleventy.config.js erstellen (Eleventy 2.x)
|
||||||
- [ ] Apex-Domain Redirect (moonweb.org → hub.moonweb.org)
|
- [x] 1.3 timecapsule/package.json erstellen (eigene Dependencies)
|
||||||
- [ ] PDF-Build fuer CV (WeasyPrint, lokal testen)
|
- [x] 1.4 timecapsule/src/_data/site.json anpassen (Domain mit /timecapsule/)
|
||||||
- [ ] Querverlinkungen stefankoelle.de <-> smarthome
|
- [x] 1.5 Alle internen Pfade mit /timecapsule/ prefixieren
|
||||||
- [ ] Ledmatrix ggf. nach smarthome verschieben
|
- [x] 1.6 Passthrough Copy in eleventy.config.js anpassen
|
||||||
- [ ] **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.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] 3.1 package.json: Neue Scripts fuer timecapsule + merge
|
||||||
- [x] Eleventy-Config + package.json erweitert
|
- [x] 3.2 scripts/merge-moonweb.sh erstellen
|
||||||
- [x] Onepager-Layout mit Anchor-Links beibehalten
|
- [x] 3.3 Lokal Build testen (alle Sites)
|
||||||
- [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)
|
|
||||||
|
|
||||||
### Vorherige Erledigungen (Stand 2026-08-13)
|
## Phase 4: Deployment umstellen
|
||||||
- [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
|
|
||||||
|
|
||||||
## 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:
|
## Phase 5: Cloudflare Redirects
|
||||||
1. Welche P4-Items sind relevant? → Nur wenn angefordert
|
|
||||||
|
|
||||||
## Definition of done
|
- [x] 5.1 Redirect-Regeln einrichten (via GitHub Action)
|
||||||
|
|
||||||
Alle 5 Sites live auf Cloudflare Pages:
|
| Quell-Domain | Ziel-URL | Type |
|
||||||
- ✅ hub.moonweb.org — verlinkt alles (+ stefankoelle.de extern)
|
|-------------|----------|------|
|
||||||
- ✅ code.moonweb.org — GitHub-Katalog via Aggregator
|
| `hub.moonweb.org/*` | `https://www.moonweb.org/$1` | 301 |
|
||||||
- ✅ smarthome.moonweb.org — Smart Home Übersicht + Detailseiten
|
| `infra.moonweb.org/*` | `https://www.moonweb.org/infra/$1` | 301 |
|
||||||
- ✅ infra.moonweb.org — Infra-Übersicht (redigiert)
|
| `smarthome.moonweb.org/*` | `https://www.moonweb.org/smarthome/$1` | 301 |
|
||||||
- ✅ retro.moonweb.org — Ehrliche minimale Übersicht (WIP)
|
| `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):
|
**Umsetzung:** `scripts/cloudflare/setup-redirects.sh` + GitHub Action `cloudflare-redirects.yml`
|
||||||
- ✅ stefankoelle.de — Onepager mit CV, Projects, Languages, Impressum
|
**Benötigte Secrets:** `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ZONE_ID`
|
||||||
- ✅ stefankoelle.de/ledmatrix/ — LED Matrix WebServer Dokumentation
|
|
||||||
|
## 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
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Page Not Found"
|
title: "Page Not Found"
|
||||||
section: "code"
|
section: "code"
|
||||||
|
tags: "code"
|
||||||
description: "The page you are looking for does not exist."
|
description: "The page you are looking for does not exist."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
permalink: /404.html
|
permalink: /code/404.html
|
||||||
---
|
---
|
||||||
<div class="error-page">
|
<div class="error-page">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
|
|||||||
@@ -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,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Open Source Projects — GitHub Repos & Code | moonweb"
|
title: "Open Source Projects — GitHub Repos & Code | moonweb"
|
||||||
section: "code"
|
section: "code"
|
||||||
|
tags: "code"
|
||||||
description: "Curated catalog of my public GitHub projects — automation, dev tools, web apps, and firmware."
|
description: "Curated catalog of my public GitHub projects — automation, dev tools, web apps, and firmware."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
subcategories:
|
subcategories:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://code.moonweb.org/sitemap.xml
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
permalink: /sitemap.xml
|
|
||||||
eleventyExcludeFromCollections: true
|
|
||||||
---
|
|
||||||
{% include "sitemap.njk" %}
|
|
||||||
@@ -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,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Page Not Found"
|
title: "Page Not Found"
|
||||||
section: "hub"
|
section: "hub"
|
||||||
|
tags: "hub"
|
||||||
description: "The page you are looking for does not exist."
|
description: "The page you are looking for does not exist."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
permalink: /404.html
|
permalink: /404.html
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Legal Notice & Privacy Policy"
|
title: "Legal Notice & Privacy Policy"
|
||||||
section: "hub"
|
section: "hub"
|
||||||
|
tags: "hub"
|
||||||
|
permalink: /impressum/index.html
|
||||||
description: "Legal notice (Impressum) and privacy policy for moonweb.org."
|
description: "Legal notice (Impressum) and privacy policy for moonweb.org."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -19,7 +21,7 @@ layout: base.njk
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -41,7 +43,7 @@ layout: base.njk
|
|||||||
|
|
||||||
<h3>2. Purpose of these Websites</h3>
|
<h3>2. Purpose of these Websites</h3>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
|
|
||||||
<h3>3. Hosting</h3>
|
<h3>3. Hosting</h3>
|
||||||
@@ -49,18 +51,23 @@ layout: base.njk
|
|||||||
Two different hosting providers are used within this network:
|
Two different hosting providers are used within this network:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4>a) moonweb.org subdomains (hub, smarthome, infra, code, retro, buildbroken) — Cloudflare Pages</h4>
|
<h4>a) www.moonweb.org (hub, infra, smarthome, code, retro, timecapsule) — IONOS SE</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>
|
|
||||||
<p>
|
<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).
|
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>
|
</p>
|
||||||
|
|
||||||
|
<h4>b) stefankoelle.de — IONOS SE</h4>
|
||||||
<p>
|
<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 — 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>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Legal basis: Art. 6(1)(f) GDPR (legitimate interest in reliable, secure delivery of the websites).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>4. No Tracking, No Cookies, No Analytics (current state)</h3>
|
<h3>4. No Tracking, No Cookies, No Analytics (current state)</h3>
|
||||||
|
|||||||
+26
-7
@@ -1,7 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "moonweb — Homelab, Smart Home, Code & Retro Hardware"
|
title: "Stefan Koelle — Homelab, Smart Home, Code & Retro Hardware | moonweb"
|
||||||
section: "hub"
|
section: "hub"
|
||||||
description: "Stefan Koelle's homelab: smart home automation, Proxmox infrastructure, open-source code projects, and retro hardware. Senior Software Developer based in Munich."
|
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
|
layout: base.njk
|
||||||
ogImage: "/stefan-koelle-foto.jpg"
|
ogImage: "/stefan-koelle-foto.jpg"
|
||||||
sections:
|
sections:
|
||||||
@@ -9,19 +11,19 @@ sections:
|
|||||||
cards:
|
cards:
|
||||||
- title: "infra"
|
- title: "infra"
|
||||||
summary: "Stack overview: Proxmox, Synology, network, backups, and how I work."
|
summary: "Stack overview: Proxmox, Synology, network, backups, and how I work."
|
||||||
href: "https://infra.moonweb.org"
|
href: "/infra/"
|
||||||
emoji: "🏗️"
|
emoji: "🏗️"
|
||||||
- title: "smarthome"
|
- title: "smarthome"
|
||||||
summary: "What's actually running on the homelab, and why."
|
summary: "What's actually running on the homelab, and why."
|
||||||
href: "https://smarthome.moonweb.org"
|
href: "/smarthome/"
|
||||||
emoji: "🏠"
|
emoji: "🏠"
|
||||||
- title: "code"
|
- title: "code"
|
||||||
summary: "Curated catalog of my GitHub projects."
|
summary: "Curated catalog of my GitHub projects."
|
||||||
href: "https://code.moonweb.org"
|
href: "/code/"
|
||||||
emoji: "💻"
|
emoji: "💻"
|
||||||
- title: "retro"
|
- title: "retro"
|
||||||
summary: "Physical retro hardware collection."
|
summary: "Physical retro hardware collection."
|
||||||
href: "https://retro.moonweb.org"
|
href: "/retro/"
|
||||||
emoji: "🕹️"
|
emoji: "🕹️"
|
||||||
- heading: "About me"
|
- heading: "About me"
|
||||||
cards:
|
cards:
|
||||||
@@ -37,7 +39,7 @@ sections:
|
|||||||
emoji: "💾"
|
emoji: "💾"
|
||||||
- title: "www.moonweb.org"
|
- title: "www.moonweb.org"
|
||||||
summary: "The 2000s internet projects, networking and engineering."
|
summary: "The 2000s internet projects, networking and engineering."
|
||||||
href: "https://www.moonweb.org"
|
href: "/timecapsule/"
|
||||||
emoji: "🌐"
|
emoji: "🌐"
|
||||||
- title: "inseco.de"
|
- title: "inseco.de"
|
||||||
summary: "Internet services & consulting for gastronomy and clubs, Peak 2003–2008. Custom PHP/CMS."
|
summary: "Internet services & consulting for gastronomy and clubs, Peak 2003–2008. Custom PHP/CMS."
|
||||||
@@ -59,3 +61,20 @@ sections:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "card-grid.njk" %}
|
{% 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 & 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -1,4 +1,5 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
Sitemap: https://hub.moonweb.org/sitemap.xml
|
Sitemap: https://www.moonweb.org/sitemap.xml
|
||||||
|
Sitemap: https://www.moonweb.org/timecapsule/sitemap.xml
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Page Not Found"
|
title: "Page Not Found"
|
||||||
section: "infra"
|
section: "infra"
|
||||||
|
tags: "infra"
|
||||||
description: "The page you are looking for does not exist."
|
description: "The page you are looking for does not exist."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
permalink: /404.html
|
permalink: /infra/404.html
|
||||||
---
|
---
|
||||||
<div class="error-page">
|
<div class="error-page">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Backup Strategy"
|
title: "Backup Strategy"
|
||||||
section: "infra"
|
section: "infra"
|
||||||
parent: "/"
|
tags: "infra"
|
||||||
|
parent: "/infra/"
|
||||||
description: "Layered 3-2-1 backup strategy covering NAS snapshots, offsite copies, and cloud storage."
|
description: "Layered 3-2-1 backup strategy covering NAS snapshots, offsite copies, and cloud storage."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Xubuntu Dev VM"
|
title: "Xubuntu Dev VM"
|
||||||
section: "infra"
|
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."
|
description: "A disposable, daily-backed-up Xubuntu VM for AI-assisted, remote development via SSH and Chrome Remote Desktop."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Docker Container Strategy"
|
title: "Docker Container Strategy"
|
||||||
section: "infra"
|
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."
|
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
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -53,11 +54,12 @@ layout: base.njk
|
|||||||
|
|
||||||
<h2>Application containers</h2>
|
<h2>Application containers</h2>
|
||||||
<ul>
|
<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>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>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>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>
|
</ul>
|
||||||
|
|
||||||
<h2>Development tools</h2>
|
<h2>Development tools</h2>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Homelab Infrastructure — Proxmox, Docker & Networking | moonweb"
|
title: "Homelab Infrastructure — Proxmox, Docker & Networking | moonweb"
|
||||||
section: "infra"
|
section: "infra"
|
||||||
|
tags: "infra"
|
||||||
description: "Homelab infrastructure overview: Proxmox hosts, Synology NAS, Docker VMs and Setup-Guides."
|
description: "Homelab infrastructure overview: Proxmox hosts, Synology NAS, Docker VMs and Setup-Guides."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
sections:
|
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>
|
<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" %}
|
{% 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
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "LXC Container Strategy"
|
title: "LXC Container Strategy"
|
||||||
section: "infra"
|
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."
|
description: "Overview of the lightweight LXC containers running on the main Proxmox host, and why they are LXC instead of Docker."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Monitoring"
|
title: "Monitoring"
|
||||||
section: "infra"
|
section: "infra"
|
||||||
parent: "/"
|
tags: "infra"
|
||||||
|
parent: "/infra/"
|
||||||
description: "Prometheus and Grafana monitoring stack for hosts, containers, and hardware metrics."
|
description: "Prometheus and Grafana monitoring stack for hosts, containers, and hardware metrics."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -9,7 +10,9 @@ layout: base.njk
|
|||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|
||||||
<p>Prometheus and Grafana form the central monitoring stack for the whole
|
<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>
|
<h2>What's monitored</h2>
|
||||||
<ul>
|
<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>
|
<tr><td>Network printer</td><td>Toner level, page count, online/offline status via SNMP</td></tr>
|
||||||
</table>
|
</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>
|
<h2>Open items</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Alerting rules are not yet configured for most services.</li>
|
<li>Alerting rules are not yet configured for most services.</li>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "OpenWrt Multi-WAN Router"
|
title: "OpenWrt Multi-WAN Router"
|
||||||
section: "infra"
|
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."
|
description: "Multi-WAN OpenWrt router in the study with WiFi and Powerline failover, plus VLAN 189 for IoT isolation."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Proxmox Virtualization"
|
title: "Proxmox Virtualization"
|
||||||
section: "infra"
|
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."
|
description: "What Proxmox VE is, the hardware it runs on in my homelab, its key advantages, and a short setup walkthrough."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://infra.moonweb.org/sitemap.xml
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
permalink: /sitemap.xml
|
|
||||||
eleventyExcludeFromCollections: true
|
|
||||||
---
|
|
||||||
{% include "sitemap.njk" %}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Synology NAS"
|
title: "Synology NAS"
|
||||||
section: "infra"
|
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."
|
description: "Model, storage setup and current role of the Synology NAS in the homelab after the late-2025 cleanup."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
+7
-14
@@ -1,23 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "moonweb-site",
|
"name": "moonweb-site",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"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": {
|
"scripts": {
|
||||||
"dev:hub": "npx @11ty/eleventy --config=hub/eleventy.config.js --serve --port=8081",
|
"dev": "npx @11ty/eleventy --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:stefankoelle": "npx @11ty/eleventy --config=stefankoelle/eleventy.config.js --serve --port=8086",
|
"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",
|
"dev:timecapsule": "cd timecapsule && npx @11ty/eleventy --serve --port=8087",
|
||||||
"build:hub": "npx @11ty/eleventy --config=hub/eleventy.config.js",
|
"build": "npx @11ty/eleventy",
|
||||||
"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",
|
|
||||||
"build:stefankoelle": "npx @11ty/eleventy --config=stefankoelle/eleventy.config.js",
|
"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",
|
"pdf:cv": "bash stefankoelle/pdf/build-pdf.sh",
|
||||||
"prebuild": "bash prebuild.sh"
|
"prebuild": "bash prebuild.sh"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Page Not Found"
|
title: "Page Not Found"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "The page you are looking for does not exist."
|
description: "The page you are looking for does not exist."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
permalink: /404.html
|
permalink: /retro/404.html
|
||||||
---
|
---
|
||||||
<div class="error-page">
|
<div class="error-page">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
---
|
---
|
||||||
title: "Running AmigaOS in 2026: MiSTer FPGA vs Amiberry on Pi"
|
title: "Running AmigaOS in 2026: MiSTer FPGA vs Amiberry on Pi"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Two very different ways to get a fully working AmigaOS 3.2.3 desktop running today."
|
description: "Two very different ways to get a fully working AmigaOS 3.2.3 desktop running today."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>Running AmigaOS in 2026: MiSTer FPGA vs Amiberry on Pi</h1>
|
<h1>Running AmigaOS in 2026: MiSTer FPGA vs Amiberry on Pi</h1>
|
||||||
<div class="detail-content">
|
<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>
|
<h2>Two Roads to the Same Workbench</h2>
|
||||||
<ul>
|
<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>
|
<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>
|
</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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Vampire V4, A600GS or MiSTer: Amiga Hardware Isn't a Simple Speed Race"
|
title: "Vampire V4, A600GS or MiSTer: Amiga Hardware Isn't a Simple Speed Race"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Why raw benchmark numbers are actively misleading when comparing modern Amiga hardware options."
|
description: "Why raw benchmark numbers are actively misleading when comparing modern Amiga hardware options."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>Vampire V4, A600GS or MiSTer: Amiga Hardware Isn't a Simple Speed Race</h1>
|
<h1>Vampire V4, A600GS or MiSTer: Amiga Hardware Isn't a Simple Speed Race</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "The Atari Mega ST Fleet: Three Machines, Three Jobs"
|
title: "The Atari Mega ST Fleet: Three Machines, Three Jobs"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "How three Atari Mega ST computers ended up with completely different roles in a modern retro setup."
|
description: "How three Atari Mega ST computers ended up with completely different roles in a modern retro setup."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>The Atari Mega ST Fleet: Three Machines, Three Jobs</h1>
|
<h1>The Atari Mega ST Fleet: Three Machines, Three Jobs</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "The 15kHz Problem: Finding a Monitor That Speaks Atari"
|
title: "The 15kHz Problem: Finding a Monitor That Speaks Atari"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Why most modern monitors refuse to display Atari ST colour modes, and which ones actually work."
|
description: "Why most modern monitors refuse to display Atari ST colour modes, and which ones actually work."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>The 15kHz Problem: Finding a Monitor That Speaks Atari</h1>
|
<h1>The 15kHz Problem: Finding a Monitor That Speaks Atari</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Getting a 40-Year-Old Computer Online: Atari ST Networking and BBS Access"
|
title: "Getting a 40-Year-Old Computer Online: Atari ST Networking and BBS Access"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Connecting an Atari ST to the modern internet and to dial-up-era bulletin board systems, warts and all."
|
description: "Connecting an Atari ST to the modern internet and to dial-up-era bulletin board systems, warts and all."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>Getting a 40-Year-Old Computer Online: Atari ST Networking and BBS Access</h1>
|
<h1>Getting a 40-Year-Old Computer Online: Atari ST Networking and BBS Access</h1>
|
||||||
<div class="detail-content">
|
<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>
|
<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>
|
</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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Batocera in the Living Room: One Box, Every Retro System"
|
title: "Batocera in the Living Room: One Box, Every Retro System"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Why the living room emulation station takes a completely different approach than the FPGA rigs upstairs."
|
description: "Why the living room emulation station takes a completely different approach than the FPGA rigs upstairs."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>Batocera in the Living Room: One Box, Every Retro System</h1>
|
<h1>Batocera in the Living Room: One Box, Every Retro System</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -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"
|
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
|
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">
|
<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>
|
<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>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>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>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>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>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>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Why an ET4000 Graphics Card and a Mega ST Don't Mix (Yet)"
|
title: "Why an ET4000 Graphics Card and a Mega ST Don't Mix (Yet)"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "A cautionary tale about pushing 1980s power supplies past their limits with a modern-ish graphics card."
|
description: "A cautionary tale about pushing 1980s power supplies past their limits with a modern-ish graphics card."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>Why an ET4000 Graphics Card and a Mega ST Don't Mix (Yet)</h1>
|
<h1>Why an ET4000 Graphics Card and a Mega ST Don't Mix (Yet)</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Retro Computing — Vintage Hardware & FPGA | moonweb"
|
title: "Retro Computing — Vintage Hardware & FPGA | moonweb"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Physical retro hardware collection — Raspberry Pi units, Pi 400, and vintage computing setups."
|
description: "Physical retro hardware collection — Raspberry Pi units, Pi 400, and vintage computing setups."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
sections:
|
sections:
|
||||||
@@ -45,8 +46,8 @@ sections:
|
|||||||
href: "/mini-arcade-handheld-roundup/"
|
href: "/mini-arcade-handheld-roundup/"
|
||||||
- heading: "DOS & PC"
|
- heading: "DOS & PC"
|
||||||
cards:
|
cards:
|
||||||
- title: "💻 The 486 DX2-66 Tower"
|
- title: "💻 The 486 DX2-66"
|
||||||
summary: "An original Colani-design 486 tower kept running alongside its MiSTer AO486 FPGA equivalent."
|
summary: "A complete DOS environment rebuilt 1:1 on MiSTer FPGA, down to the original AUTOEXEC.BAT."
|
||||||
href: "/dos-486-tower/"
|
href: "/dos-486-tower/"
|
||||||
- heading: "Overview"
|
- heading: "Overview"
|
||||||
cards:
|
cards:
|
||||||
@@ -55,6 +56,6 @@ sections:
|
|||||||
href: "/retro-corner-snapshot/"
|
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" %}
|
{% include "card-grid.njk" %}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Mini Arcades and Handhelds: Sorting the Real Deal from the Bootlegs"
|
title: "Mini Arcades and Handhelds: Sorting the Real Deal from the Bootlegs"
|
||||||
section: "retro"
|
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."
|
description: "A buyer's guide to compact retro gaming devices, and why the cheapest option is almost never the right one."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>Mini Arcades and Handhelds: Sorting the Real Deal from the Bootlegs</h1>
|
<h1>Mini Arcades and Handhelds: Sorting the Real Deal from the Bootlegs</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "MiST and MiSTer: Two FPGA Boxes, Two Very Different Personalities"
|
title: "MiST and MiSTer: Two FPGA Boxes, Two Very Different Personalities"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Why the newer, more powerful MiSTer isn't automatically the better choice for every retro system."
|
description: "Why the newer, more powerful MiSTer isn't automatically the better choice for every retro system."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>MiST and MiSTer: Two FPGA Boxes, Two Very Different Personalities</h1>
|
<h1>MiST and MiSTer: Two FPGA Boxes, Two Very Different Personalities</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "PiMiga 3 to 5: Chasing the Perfect Amiga-on-a-Pi Distro"
|
title: "PiMiga 3 to 5: Chasing the Perfect Amiga-on-a-Pi Distro"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "Three generations of PiMiga later, here's what actually changed — and why newer isn't always better."
|
description: "Three generations of PiMiga later, here's what actually changed — and why newer isn't always better."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>PiMiga 3 to 5: Chasing the Perfect Amiga-on-a-Pi Distro</h1>
|
<h1>PiMiga 3 to 5: Chasing the Perfect Amiga-on-a-Pi Distro</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "The Retro Corner Today: A Snapshot of the Whole Setup"
|
title: "The Retro Corner Today: A Snapshot of the Whole Setup"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "A tour through every retro system currently active across the home, room by room."
|
description: "A tour through every retro system currently active across the home, room by room."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>The Retro Corner Today: A Snapshot of the Whole Setup</h1>
|
<h1>The Retro Corner Today: A Snapshot of the Whole Setup</h1>
|
||||||
<div class="detail-content">
|
<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>
|
<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>
|
</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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://retro.moonweb.org/sitemap.xml
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
permalink: /sitemap.xml
|
|
||||||
eleventyExcludeFromCollections: true
|
|
||||||
---
|
|
||||||
{% include "sitemap.njk" %}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "The A1200: What We Know Before It Ships"
|
title: "The A1200: What We Know Before It Ships"
|
||||||
section: "retro"
|
section: "retro"
|
||||||
|
tags: "retro"
|
||||||
description: "A close look at Retro Games Ltd.'s upcoming Amiga 1200 console, based on everything public so far."
|
description: "A close look at Retro Games Ltd.'s upcoming Amiga 1200 console, based on everything public so far."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/retro/"
|
||||||
---
|
---
|
||||||
<h1>The A1200: What We Know Before It Ships</h1>
|
<h1>The A1200: What We Know Before It Ships</h1>
|
||||||
<div class="detail-content">
|
<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>
|
<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>
|
</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>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Executable
+172
@@ -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
|
||||||
@@ -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:title" content="{{ title }}{% if 'moonweb' not in title and 'stefankoelle' not in title %} · moonweb.org{% endif %}">
|
||||||
<meta name="twitter:description" content="{{ description }}">
|
<meta name="twitter:description" content="{{ description }}">
|
||||||
{% if ogImage %}<meta name="twitter:image" content="{{ site.url }}{{ ogImage }}">{% endif %}
|
{% 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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Lobster&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Lobster&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/shared-base.css">
|
<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">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -39,6 +38,29 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{% if section == 'hub' and not parent %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Stefan Koelle",
|
||||||
|
"url": "https://www.moonweb.org",
|
||||||
|
"image": "https://www.moonweb.org/stefan-koelle-foto.jpg",
|
||||||
|
"jobTitle": "Senior Software Developer & Architect",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"addressLocality": "Munich",
|
||||||
|
"addressCountry": "DE"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://stefankoelle.de",
|
||||||
|
"https://www.linkedin.com/in/stefankoelle/",
|
||||||
|
"https://github.com/skoelle",
|
||||||
|
"https://www.xing.com/profile/Stefan_Koelle"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
@@ -46,11 +68,11 @@
|
|||||||
<span class="site-title">{{ section }}.moonweb.org</span>
|
<span class="site-title">{{ section }}.moonweb.org</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-switcher">
|
<nav class="site-switcher">
|
||||||
<a href="https://hub.moonweb.org">hub</a>
|
<a href="/">hub</a>
|
||||||
<a href="https://infra.moonweb.org">infra</a>
|
<a href="/infra/">infra</a>
|
||||||
<a href="https://smarthome.moonweb.org">smarthome</a>
|
<a href="/smarthome/">smarthome</a>
|
||||||
<a href="https://code.moonweb.org">code</a>
|
<a href="/code/">code</a>
|
||||||
<a href="https://retro.moonweb.org">retro</a>
|
<a href="/retro/">retro</a>
|
||||||
<a href="https://stefankoelle.de">cv</a>
|
<a href="https://stefankoelle.de">cv</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -61,8 +83,8 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<p>Part of the <a href="https://hub.moonweb.org">moonweb.org</a> homelab.</p>
|
<p>Part of the <a href="/">moonweb.org</a> homelab.</p>
|
||||||
<p><a href="https://hub.moonweb.org/impressum/">Legal Notice & Privacy Policy</a></p>
|
<p><a href="/impressum/">Legal Notice & Privacy Policy</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -214,3 +214,30 @@ main { max-width: 1100px; margin: 0 auto; padding: 2rem; }
|
|||||||
color: #666;
|
color: #666;
|
||||||
margin: 0 0 2rem;
|
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
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Page Not Found"
|
title: "Page Not Found"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
|
tags: "smarthome"
|
||||||
description: "The page you are looking for does not exist."
|
description: "The page you are looking for does not exist."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
permalink: /404.html
|
permalink: /smarthome/404.html
|
||||||
---
|
---
|
||||||
<div class="error-page">
|
<div class="error-page">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "AirPlay Audio"
|
title: "AirPlay Audio"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
parent: "/"
|
tags: "smarthome"
|
||||||
|
parent: "/smarthome/"
|
||||||
description: "Multi-room AirPlay receivers with software volume boost on Raspberry Pi devices."
|
description: "Multi-room AirPlay receivers with software volume boost on Raspberry Pi devices."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Balkon Pi"
|
title: "Balkon Pi"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
parent: "/"
|
tags: "smarthome"
|
||||||
|
parent: "/smarthome/"
|
||||||
description: "Balcony sensor, lighting, and audio automation running on a Raspberry Pi Zero W."
|
description: "Balcony sensor, lighting, and audio automation running on a Raspberry Pi Zero W."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -11,7 +12,10 @@ layout: base.njk
|
|||||||
<p>A Raspberry Pi Zero W mounted near the balcony acts as a small,
|
<p>A Raspberry Pi Zero W mounted near the balcony acts as a small,
|
||||||
self-contained smart-balcony controller: light control, an internet
|
self-contained smart-balcony controller: light control, an internet
|
||||||
radio player, sensor readings, and its own backup/monitoring, all on
|
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>
|
<h2>What it does</h2>
|
||||||
<ul>
|
<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>
|
<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>
|
</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>
|
<h2>Monitoring & backup, even on tiny hardware</h2>
|
||||||
<p>Despite the very limited RAM, this Pi still participates in the same
|
<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
|
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>
|
<ul>
|
||||||
<li>Adding a USB webcam stream, mirroring the pattern already used for the 3D printer.</li>
|
<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>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>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Home Control Buttons"
|
title: "Home Control Buttons"
|
||||||
section: "smarthome"
|
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."
|
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
|
layout: base.njk
|
||||||
parent: "/"
|
parent: "/smarthome/"
|
||||||
---
|
---
|
||||||
<h1>Home Control Buttons</h1>
|
<h1>Home Control Buttons</h1>
|
||||||
<div class="detail-content">
|
<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>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>
|
<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>
|
</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 & status</h2>
|
||||||
<ul>
|
<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>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>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 & version info</strong> — direct links into the Healthchecks dashboard and a version/status overview for the connected devices.</li>
|
<li><strong>Healthchecks & version info</strong> — direct links into the Healthchecks dashboard and a version/status overview for the connected devices.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h2>Calendar & 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>
|
<h2>Network shortcuts</h2>
|
||||||
<ul>
|
<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>
|
<li><strong>Routers & mesh</strong> — quick links to the main Fritzbox and its mesh repeaters covering different floors and the kitchen/IoT band.</li>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "HomematicIP + MQTT"
|
title: "HomematicIP + MQTT"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
parent: "/"
|
tags: "smarthome"
|
||||||
|
parent: "/smarthome/"
|
||||||
description: "How HomematicIP room sensors are bridged into MQTT and stored in InfluxDB for dashboards."
|
description: "How HomematicIP room sensors are bridged into MQTT and stored in InfluxDB for dashboards."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -29,11 +30,39 @@ sensor state changes to MQTT.</p>
|
|||||||
<li>Grafana reads from InfluxDB for the temperature/humidity dashboards.</li>
|
<li>Grafana reads from InfluxDB for the temperature/humidity dashboards.</li>
|
||||||
</ol>
|
</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>
|
<h2>Why this is worth documenting</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>No extra Docker container or unmaintained library dependency.</li>
|
<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>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 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>
|
</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>
|
</div>
|
||||||
|
|||||||
+23
-2
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Smart Home Projects — Home Assistant, MQTT & IoT | moonweb"
|
title: "Smart Home Projects — Home Assistant, MQTT & IoT | moonweb"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
|
tags: "smarthome"
|
||||||
description: "Smart home projects: Home Assistant, MQTT sensors, AirPlay audio, OctoPrint, and energy monitoring."
|
description: "Smart home projects: Home Assistant, MQTT sensors, AirPlay audio, OctoPrint, and energy monitoring."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
sections:
|
sections:
|
||||||
@@ -36,6 +37,10 @@ sections:
|
|||||||
summary: "ESP32 dashboard showing weather, calendar, and MVG departures."
|
summary: "ESP32 dashboard showing weather, calendar, and MVG departures."
|
||||||
href: "https://github.com/skoelle/m5stack-dashboard"
|
href: "https://github.com/skoelle/m5stack-dashboard"
|
||||||
emoji: "🖥️"
|
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"
|
- heading: "Personal services"
|
||||||
cards:
|
cards:
|
||||||
- title: "iCloud Contacts Sync"
|
- title: "iCloud Contacts Sync"
|
||||||
@@ -64,11 +69,27 @@ sections:
|
|||||||
summary: "Remote 3D printer control and camera streaming."
|
summary: "Remote 3D printer control and camera streaming."
|
||||||
href: "/octoprint/"
|
href: "/octoprint/"
|
||||||
emoji: "🖨️"
|
emoji: "🖨️"
|
||||||
- title: "TubeArchivist"
|
- title: "YouTube archiving"
|
||||||
summary: "Self-hosted YouTube archiving running on the Synology."
|
summary: "TubeArchivist and TubeSync for self-hosted YouTube downloading on the Synology."
|
||||||
href: "/tubearchivist/"
|
href: "/tubearchivist/"
|
||||||
emoji: "📺"
|
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>
|
<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" %}
|
{% 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "OctoPrint"
|
title: "OctoPrint"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
parent: "/"
|
tags: "smarthome"
|
||||||
|
parent: "/smarthome/"
|
||||||
description: "Remote 3D printer control and camera streaming with OctoPrint on Raspberry Pi."
|
description: "Remote 3D printer control and camera streaming with OctoPrint on Raspberry Pi."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -10,7 +11,9 @@ layout: base.njk
|
|||||||
|
|
||||||
<p>A Raspberry Pi 3B runs OctoPrint for remote 3D-printer control, plus a
|
<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
|
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>
|
<h2>Current setup</h2>
|
||||||
<ul>
|
<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>
|
<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>
|
</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>
|
<h2>History</h2>
|
||||||
<p>This Pi originally also had a small status display attached; the
|
<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,
|
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
|
<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
|
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
|
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>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://smarthome.moonweb.org/sitemap.xml
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
permalink: /sitemap.xml
|
|
||||||
eleventyExcludeFromCollections: true
|
|
||||||
---
|
|
||||||
{% include "sitemap.njk" %}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "Tasmota & Energy Monitoring"
|
title: "Tasmota & Energy Monitoring"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
parent: "/"
|
tags: "smarthome"
|
||||||
|
parent: "/smarthome/"
|
||||||
description: "Smart plugs and power monitoring across the flat with daily usage statistics."
|
description: "Smart plugs and power monitoring across the flat with daily usage statistics."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
@@ -10,7 +11,9 @@ layout: base.njk
|
|||||||
|
|
||||||
<p>Power monitoring across the flat runs on a mix of Tasmota-flashed smart
|
<p>Power monitoring across the flat runs on a mix of Tasmota-flashed smart
|
||||||
plugs, tracking accumulated kWh per device and feeding daily usage
|
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>
|
<h2>What's metered</h2>
|
||||||
<table>
|
<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
|
attempt due to a transient Wi-Fi handshake issue — retrying resolves it
|
||||||
without any special handling.</p>
|
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>
|
<h2>What this data is used for</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Spotting appliances with unexpectedly high standby draw.</li>
|
<li>Spotting appliances with unexpectedly high standby draw.</li>
|
||||||
<li>Sanity-checking that "turned off" devices are actually drawing near-zero power.</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>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>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,44 @@
|
|||||||
---
|
---
|
||||||
title: "TubeArchivist"
|
title: "YouTube Archiving: TubeArchivist & TubeSync"
|
||||||
section: "smarthome"
|
section: "smarthome"
|
||||||
parent: "/"
|
tags: "smarthome"
|
||||||
description: "Self-hosted YouTube archiving and media management running on the Synology NAS."
|
parent: "/smarthome/"
|
||||||
|
description: "Self-hosted YouTube archiving and media management running on the Synology NAS, comparing TubeArchivist and TubeSync."
|
||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
<h1>TubeArchivist</h1>
|
<h1>YouTube Archiving: TubeArchivist & TubeSync</h1>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
|
|
||||||
<p>TubeArchivist runs on the Synology NAS via Docker Compose, self-hosting
|
<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>
|
||||||
a searchable archive of downloaded YouTube content (Elasticsearch +
|
|
||||||
Redis + the TubeArchivist app itself).</p>
|
|
||||||
|
|
||||||
<h2>Why this needed real troubleshooting</h2>
|
<h2>TubeArchivist</h2>
|
||||||
<p>Running Elasticsearch on Synology's DSM kernel isn't fully
|
<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>
|
||||||
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>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>
|
<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>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>
|
</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>
|
<table>
|
||||||
<tr><th>Data</th><th>Included in NAS backup?</th></tr>
|
<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>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>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>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,11 +26,11 @@
|
|||||||
<span class="site-title">{{ siteTitle | default(section + ".moonweb.org") }}</span>
|
<span class="site-title">{{ siteTitle | default(section + ".moonweb.org") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-switcher">
|
<nav class="site-switcher">
|
||||||
<a href="https://hub.moonweb.org">hub</a>
|
<a href="https://www.moonweb.org/">hub</a>
|
||||||
<a href="https://infra.moonweb.org">infra</a>
|
<a href="https://www.moonweb.org/infra/">infra</a>
|
||||||
<a href="https://smarthome.moonweb.org">smarthome</a>
|
<a href="https://www.moonweb.org/smarthome/">smarthome</a>
|
||||||
<a href="https://code.moonweb.org">code</a>
|
<a href="https://www.moonweb.org/code/">code</a>
|
||||||
<a href="https://retro.moonweb.org">retro</a>
|
<a href="https://www.moonweb.org/retro/">retro</a>
|
||||||
<a href="https://stefankoelle.de">cv</a>
|
<a href="https://stefankoelle.de">cv</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<p>Part of the <a href="https://hub.moonweb.org">moonweb.org</a> homelab.</p>
|
<p>Part of the <a href="https://www.moonweb.org/">moonweb.org</a> homelab.</p>
|
||||||
<p><a href="https://hub.moonweb.org/impressum/">Legal Notice & Privacy Policy</a></p>
|
<p><a href="https://www.moonweb.org/impressum/">Legal Notice & Privacy Policy</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"url": "{{ site.url }}",
|
"url": "{{ site.url }}",
|
||||||
"jobTitle": "Software Developer",
|
"jobTitle": "Software Developer",
|
||||||
"sameAs": [
|
"sameAs": [
|
||||||
|
"https://www.moonweb.org/",
|
||||||
"https://www.linkedin.com/in/stefankoelle/",
|
"https://www.linkedin.com/in/stefankoelle/",
|
||||||
"https://github.com/skoelle",
|
"https://github.com/skoelle",
|
||||||
"https://www.xing.com/profile/Stefan_Koelle"
|
"https://www.xing.com/profile/Stefan_Koelle"
|
||||||
|
|||||||
+24
-14
@@ -22,23 +22,33 @@ layout: stefankoelle.njk
|
|||||||
<div id="personalprojectscontent">
|
<div id="personalprojectscontent">
|
||||||
<h2>Personal Projects</h2>
|
<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>
|
<h3>Home Network Projects (2025-2026)</h3>
|
||||||
<ul>
|
<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>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>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>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://smarthome.moonweb.org/tasmota-energy/">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://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>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>
|
</ul>
|
||||||
|
|
||||||
<h3>Retro Computing (2022/2023)</h3>
|
<h3>Retro Computing (2022/2023)</h3>
|
||||||
<ul>
|
<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>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>DOS System, Amiga System, and Atari ST System with the environment from the early 90s.</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>
|
</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>
|
<ul>
|
||||||
<li>Gotek Drive</li>
|
<li>Gotek Drive</li>
|
||||||
<li>More RAM</li>
|
<li>More RAM</li>
|
||||||
@@ -57,10 +67,10 @@ layout: stefankoelle.njk
|
|||||||
|
|
||||||
<h3>Various Docker Systems (since 2016)</h3>
|
<h3>Various Docker Systems (since 2016)</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>Monitoring:</b> Prometheus, Grafana, InfluxDB, cAdvisor, Node Exporter, SNMP exporters</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, Home Assistant</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>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>
|
</ul>
|
||||||
|
|
||||||
<h3>Walk-on Song (2019)</h3>
|
<h3>Walk-on Song (2019)</h3>
|
||||||
@@ -70,7 +80,7 @@ layout: stefankoelle.njk
|
|||||||
<li>MP3 Module for playback on attached speaker.</li>
|
<li>MP3 Module for playback on attached speaker.</li>
|
||||||
</ul>
|
</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>
|
<ul>
|
||||||
<li>Interactive audio playback system utilizing RFID technology.</li>
|
<li>Interactive audio playback system utilizing RFID technology.</li>
|
||||||
<li>Powered by an Arduino Nano for compact and efficient operation.</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>
|
<h3>Mailbox/Fidonet (1994 - 2000)</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Operated a BBS and Fidonet Node (see also <a href="https://retro.moonweb.org/atari-networking-bbs/">Networking & 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>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>Software was running on an OS/2 computer.</li>
|
||||||
<li>Utilized 2x ISDN and 1x MODEM connections.</li>
|
<li>Utilized 2x ISDN and 1x MODEM connections.</li>
|
||||||
@@ -136,7 +146,7 @@ layout: stefankoelle.njk
|
|||||||
|
|
||||||
<h3>AI-Assisted Development</h3>
|
<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>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>
|
<h3>.NET Core</h3>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -233,5 +243,5 @@ layout: stefankoelle.njk
|
|||||||
<p>Stefan Kölle<br>Neumarkter Str. 86c<br>81673 München</p>
|
<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>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>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>
|
</div>
|
||||||
|
|||||||
@@ -134,14 +134,14 @@ description: "WiFi-controlled LED display solution with REST API, DHT22 sensor,
|
|||||||
<div class="feature-icon">📊</div>
|
<div class="feature-icon">📊</div>
|
||||||
<div class="feature-text">
|
<div class="feature-text">
|
||||||
<h3>Sensor Readings</h3>
|
<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>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">🔔</div>
|
<div class="feature-icon">🔔</div>
|
||||||
<div class="feature-text">
|
<div class="feature-text">
|
||||||
<h3>Status Messages</h3>
|
<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>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
@@ -575,7 +575,7 @@ ESP32 will be restarted (with display shutdown)</pre>
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<p>© 2026 <a href="https://stefankoelle.de/">Stefan Kölle</a>. LED Matrix WebServer v1.08.</p>
|
<p>© 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 & Privacy Policy</a></p>
|
<p><a href="https://www.moonweb.org//impressum/">Legal Notice & Privacy Policy</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ echo "==> Installing WeasyPrint..."
|
|||||||
pip install -q -r "$REQUIREMENTS"
|
pip install -q -r "$REQUIREMENTS"
|
||||||
|
|
||||||
echo "==> Building stefankoelle with Eleventy..."
|
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"
|
npx @11ty/eleventy --config="$PROJECT_DIR/stefankoelle/eleventy.config.js"
|
||||||
|
|
||||||
echo "==> Generating PDF..."
|
echo "==> Generating PDF..."
|
||||||
|
|||||||
Binary file not shown.
@@ -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",
|
||||||
|
};
|
||||||
|
};
|
||||||
Generated
+2505
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
ErrorDocument 404 /404/index.html
|
||||||
@@ -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> </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>
|
||||||
@@ -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
Reference in New Issue
Block a user