Compare commits
5 Commits
claude/ela
...
claude/jol
| Author | SHA1 | Date | |
|---|---|---|---|
| a45e4f6f29 | |||
| 77b3a6f677 | |||
| 41df77fb20 | |||
| b67389b137 | |||
| 45b7813eb7 |
112
containers/131-teddycloud.md
Normal file
112
containers/131-teddycloud.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 131 — `teddycloud`
|
||||
|
||||
Open-source replacement server for Toniebox smart audio devices (Tonieboxes). Serves device content and API on port 443 and exposes a management web UI at `teddy.hubris.network`.
|
||||
|
||||
## At a glance
|
||||
|
||||
- **Hostname:** `teddycloud`
|
||||
- **IP:** `192.168.8.243` (DHCP reservation; MAC `bc:24:11:11:7a:df`)
|
||||
- **Privilege:** privileged
|
||||
- **Resources:** 1 core / 1 GiB RAM / 16 GiB rootfs (Debian 12)
|
||||
- **Mounts:** `/mnt/library` (`mp0`) — TeddyCloud content at `/mnt/library/cloud/leon`
|
||||
- **Public hostname:** none (LAN-only)
|
||||
|
||||
## Role
|
||||
|
||||
Replaces the Boxine cloud (`prod.de.bb-online.com`) as the backend for Leon's Toniebox. Tonieboxes connect on port 443 using a custom CA cert issued by TeddyCloud. Content (Tonies) is stored on the NAS at `/mnt/library/cloud/leon` and is accessible from the management UI.
|
||||
|
||||
## Service / port map
|
||||
|
||||
| Service | Listen | Notes |
|
||||
|---------|--------|-------|
|
||||
| TeddyCloud device API | `0.0.0.0:443` | HTTPS, TeddyCloud self-signed CA, Toniebox connects here |
|
||||
| TeddyCloud HTTP | `0.0.0.0:80` | Redirects to 443 |
|
||||
| TeddyCloud web UI | `0.0.0.0:8443` | HTTPS management UI — fronted by Caddy at `teddy.hubris.network` (backend uses `tls_insecure_skip_verify` for self-signed cert on LAN hop) |
|
||||
|
||||
## Docker Compose
|
||||
|
||||
`/opt/teddycloud/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
teddycloud:
|
||||
image: ghcr.io/toniebox-reverse-engineering/teddycloud:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8443:8443"
|
||||
volumes:
|
||||
- certs:/teddycloud/certs
|
||||
- config:/teddycloud/config
|
||||
- /mnt/library/cloud/leon:/teddycloud/content
|
||||
- /mnt/library/cloud/leon:/teddycloud/library
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
certs:
|
||||
config:
|
||||
```
|
||||
|
||||
`certs` and `config` are Docker named volumes (runtime state). `content` and `library` are bind-mounted from `/mnt/library/cloud/leon` so audio content persists across container rebuilds and is browsable from the host.
|
||||
|
||||
## Storage / config paths
|
||||
|
||||
- `/opt/teddycloud/docker-compose.yml` — compose file
|
||||
- Docker volume `teddycloud_certs` — TeddyCloud CA + server certs (generated on first boot)
|
||||
- Docker volume `teddycloud_config` — TeddyCloud config
|
||||
- `/mnt/library/cloud/leon/` — Tonie content + library (NAS bind mount)
|
||||
|
||||
## Networking
|
||||
|
||||
Two separate traffic paths — different IPs, no port 443 conflict:
|
||||
|
||||
**Management UI (browser):**
|
||||
```
|
||||
teddy.hubris.network → Technitium → 192.168.8.175 (Caddy) → 192.168.8.243:8443
|
||||
```
|
||||
|
||||
**Toniebox device traffic:**
|
||||
```
|
||||
prod.de.bb-online.com → Technitium override → 192.168.8.243:443 (TeddyCloud direct)
|
||||
```
|
||||
|
||||
Caddy terminates TLS for the management UI (IONOS DNS-01 wildcard cert). TeddyCloud terminates TLS for device traffic with its own self-signed CA — the Toniebox must have this CA installed.
|
||||
|
||||
### DNS overrides in Technitium
|
||||
|
||||
| Record | Type | Value | Purpose |
|
||||
|--------|------|-------|---------|
|
||||
| `teddy.hubris.network` | A | `192.168.8.175` | Management UI → Caddy (standard pattern) |
|
||||
| `prod.de.bb-online.com` | A | `192.168.8.243` | Toniebox device traffic → TeddyCloud direct |
|
||||
|
||||
The `prod.de.bb-online.com` override is Technitium-only — it intercepts Toniebox DNS locally without touching public DNS. The `dns-sync.py` cron on LXC 107 skips non-`hubris.network` records, so it stays local.
|
||||
|
||||
## Config notes
|
||||
|
||||
- `core.boxCertAuth=false` — client cert validation disabled. The box connects without presenting its unique client cert. Set in `/var/lib/docker/volumes/teddycloud_config/_data/config.ini` (TeddyCloud hot-reloads on change).
|
||||
- If you ever want per-box auth, flip to `true` and supply `certs/client/ca.der`, `client.der`, `private.der` extracted from the box flash.
|
||||
|
||||
## Toniebox onboarding — ESP32 SD card method
|
||||
|
||||
Leon's box is ESP32 generation. No hardware mod required.
|
||||
|
||||
1. Download the TeddyCloud CA cert from the web UI: **Security → CA Certificate → Download CA** (`ca.der`).
|
||||
2. Power off the Toniebox, remove the SD card.
|
||||
3. On the SD card, create folder `cert/` at the root.
|
||||
4. Copy the downloaded `ca.der` into `cert/ca.der` on the SD card.
|
||||
5. Reinsert SD card, power on the box.
|
||||
6. The box patches itself to trust TeddyCloud's CA, then resolves `prod.de.bb-online.com` via Technitium's override (`192.168.8.243`) and connects on port 443.
|
||||
|
||||
Reference: [upstream wiki — ESP32 SD card method](https://github.com/toniebox-reverse-engineering/teddycloud/wiki).
|
||||
|
||||
## Related
|
||||
|
||||
- [Caddy (121)](121-caddy.md) — LAN reverse proxy (`teddy.hubris.network → 192.168.8.243:8443`)
|
||||
- [DNS (107)](../infrastructure/dns.md) — Technitium A records for `teddy.hubris.network` and `prod.de.bb-online.com`
|
||||
- [Media permissions](../infrastructure/media-permissions.md) — NAS `/mnt/library` mount pattern
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-06-29 — provisioned
|
||||
|
||||
LXC 131 created (Debian 12, privileged, nesting=1). Docker installed. TeddyCloud running via Docker Compose at `/opt/teddycloud/`. Content bind-mounted from `/mnt/library/cloud/leon`. Caddy block added at `teddy.hubris.network → :8443`. Technitium A records: `teddy.hubris.network → 192.168.8.175` (Caddy), `prod.de.bb-online.com → 192.168.8.243` (device traffic direct).
|
||||
@@ -18,6 +18,7 @@ All containers live on [`hubris`](../hosts/hubris.md). Each row links to the per
|
||||
| 128 | [trmnl](128-trmnl.md) | 192.168.8.211 | unpriv | 1 | 768 MiB | 8 GiB | — | `trmnl.hubris.network` | running |
|
||||
| 129 | [house](129-house.md) | 192.168.8.212 | unpriv | 1 | 1344 MiB | 8 GiB | — | `house.hubris.network` | running |
|
||||
| 130 | [grimmory](130-grimmory.md) | 192.168.8.213 | priv | 1 | 2 GiB | 16 GiB | `/mnt/library` | `books.hubris.network` | running |
|
||||
| 131 | [teddycloud](131-teddycloud.md) | 192.168.8.243 | priv | 1 | 1 GiB | 16 GiB | `/mnt/library` | `teddy.hubris.network` (LAN only) | running |
|
||||
|
||||
## Recently destroyed (kept for archaeology)
|
||||
|
||||
|
||||
@@ -33,9 +33,18 @@ address=/photos.hubris.network/192.168.8.175
|
||||
address=/photos-new.hubris.network/192.168.8.175
|
||||
address=/artifacto.hubris.network/192.168.8.175
|
||||
address=/zimaos.hubris.network/192.168.8.175
|
||||
address=/teddy.hubris.network/192.168.8.175
|
||||
address=/nfs-export.hubris.network/192.168.8.200
|
||||
```
|
||||
|
||||
**Non-`hubris.network` override (Toniebox device traffic):**
|
||||
|
||||
```
|
||||
address=/prod.de.bb-online.com/192.168.8.243 # → TeddyCloud (131) direct on :443
|
||||
```
|
||||
|
||||
This intercepts Toniebox DNS locally without touching public DNS. The `dns-sync.py` cron on LXC 107 skips non-`hubris.network` records — this entry is Technitium-only.
|
||||
|
||||
Note: `nfs-export.hubris.network` is the only `.hubris.network` entry that points to a non-HTTP service (NFSv4 on port 2049). It bypasses [caddy (121)](../containers/121-caddy.md) because NFS is L4, not HTTP — Caddy has nothing to do.
|
||||
|
||||
## Why split-horizon
|
||||
@@ -75,6 +84,9 @@ Either:
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-06-29 — `teddy.hubris.network` added; `prod.de.bb-online.com` override added
|
||||
TeddyCloud (LXC 131) provisioned. `teddy.hubris.network → 192.168.8.175` (Caddy → TeddyCloud web UI at :8443). Non-hubris override `prod.de.bb-online.com → 192.168.8.243` routes Toniebox device HTTPS traffic directly to TeddyCloud port 443 — this bypasses Caddy and is Technitium-only (dns-sync cron does not replicate non-hubris.network records to the NetBird managed zone).
|
||||
|
||||
### 2026-06-28 — `plato.hubris.network` removed
|
||||
Plato (LXC 126) decommissioned. Technitium entry deleted; dns-sync cron reaped the NetBird managed zone record.
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ hosts:
|
||||
notes:
|
||||
- Docker host for Grimmory (community fork of Booklore). Created 2026-06-29.
|
||||
- Migrated from apps LXC 105; MariaDB data carried over (schema-compatible fork).
|
||||
age_pubkey: '' # filled by homelab client add --finalize-pubkey
|
||||
age_pubkey: age1uellsemnjrzgfg9fxw4jefpy05laxzggwnwhh6ny3wl7alyp6v8q0muxet
|
||||
# 123 (claudio-bot) — destroyed 2026-06-04, replaced by Hermes Agent
|
||||
# 126 (plato) — destroyed 2026-06-28, notes workspace decommissioned
|
||||
zimaos:
|
||||
|
||||
202
operations/runbook-budget-from-csv.md
Normal file
202
operations/runbook-budget-from-csv.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Runbook: Budget import from N26 CSV → Yuvomi
|
||||
|
||||
Distil a bank-export CSV into Yuvomi's Budget and Subscriptions modules using
|
||||
the `yuvomi-mcp` tools. Run this whenever a new CSV period needs to be
|
||||
summarised into targets and fixed costs.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `yuvomi-mcp` is running on LXC 129 and connected as an MCP server in Claude.
|
||||
- The CSV is an N26 export (columns: Booking Date, Value Date, Partner Name,
|
||||
Partner Iban, Type, Payment Reference, Account Name, Amount (EUR), …).
|
||||
- API token: `homelab secret yuvomi-api-token` (decrypts on any enrolled client).
|
||||
- Direct API base: `https://house.hubris.network/api/v1`
|
||||
|
||||
---
|
||||
|
||||
## API quirks (Yuvomi ≤ 0.77.x)
|
||||
|
||||
- **Subscriptions live under `/budget/subscriptions`**, NOT `/subscriptions/`.
|
||||
A top-level `/subscriptions` route returns 404.
|
||||
- `GET /budget/subscriptions` → `{ data: { subscriptions: [...], summary: {...} } }`
|
||||
- `GET /budget/subscriptions/meta` → `{ data: { categories: [...], payment_methods: [...] } }`
|
||||
- `POST /budget/subscriptions` → create a subscription (name, amount, billing_cycle,
|
||||
cycle_interval, next_payment_date, currency, category_id, payment_method_id required)
|
||||
- `GET /budget/` (no month) → returns only **non-recurring** base entries.
|
||||
Use `GET /budget/?month=YYYY-MM` to get all entries (recurring + one-time) for a month.
|
||||
- `GET /budget/categories` → expense category keys + income category names (German keys
|
||||
like `"Erwerbseinkommen"`, `"Sozialleistungen"`, `"Geschenke & Transfers"`).
|
||||
- Budget entries: `amount` positive = income, negative = expense.
|
||||
- Recurring entries: set `is_recurring: 1` + `recurrence_interval: "monthly"`.
|
||||
The `date` field sets the start month.
|
||||
- `recurrence_virtual: 1` smooths non-monthly amounts across all months in the summary
|
||||
(e.g. 55.08 € quarterly → shows as ~18.36 €/month).
|
||||
- Custom RRULE strings (`recurrence_rule`) are **not accepted** by the API — use
|
||||
`cycle_interval` on the subscription instead, or `recurrence_interval` on budget entries.
|
||||
|
||||
---
|
||||
|
||||
## Subscription category IDs (as of 2026-06-26)
|
||||
|
||||
| id | name | budget_subcategory_key |
|
||||
|---|---|---|
|
||||
| 1 | Entertainment | subscription_entertainment |
|
||||
| 2 | Productivity | subscription_productivity |
|
||||
| 3 | Utilities | subscription_utilities |
|
||||
| 4 | Health | subscription_health |
|
||||
| 5 | Education | subscription_education |
|
||||
| 6 | Other | subscription_other |
|
||||
|
||||
## Payment method IDs
|
||||
|
||||
| id | name |
|
||||
|---|---|
|
||||
| 1 | Credit Card |
|
||||
| 2 | Debit Card |
|
||||
| 3 | PayPal |
|
||||
| 6 | Bank Transfer / SEPA |
|
||||
| 7 | Other |
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Categorise the transactions
|
||||
|
||||
Skip these as internal/already-covered:
|
||||
- Fixed costs you'll enter as **subscriptions** (Miete, SWM, SYNVIA, Hundefutter,
|
||||
Netflix, Grover, Rundfunk ARD, KuKita, Lillydoo)
|
||||
- Internal transfers (The Joy Pot ↔ Cookie, Hauptkonto, Tagesgeldkonto splits)
|
||||
- Identified income (Cookie Share, Kindergeld, Pocket Money credits, Distributor)
|
||||
- Fun Money pass-throughs (in and out same month → net zero)
|
||||
|
||||
**Variable expense taxonomy:**
|
||||
|
||||
| Category key | Subcategory key | Examples |
|
||||
|---|---|---|
|
||||
| `food` | `groceries` | E-Center, Knuspr, EDEKA, Tegut, VollCorner, Lidl, Netto, REWE, KoRo, Roast Market |
|
||||
| `food` | `restaurants_bars` | Restaurants, Lieferando, Cafes, Zeit für Brot, Baobab, Höflinger |
|
||||
| `personal_health` | `beauty_cosmetics` | DM, Rossmann |
|
||||
| `personal_health` | `pharmacy` | Apotheke, MVZ Dermatologie |
|
||||
| `transport` | `apps_taxi` | Uber, RYD GMBH, MVG, Handyparken |
|
||||
| `shopping_clothing` | `gifts` | Children products: Schlummersack, Catchy Kids, SP EVERY., Dukal, Berger-Lernwelt |
|
||||
| `shopping_clothing` | `clothes_shoes` | Zalando, Ernsting's, Schuhmair, Thalia, Vinted, Airbnb, Hotel at Booking.com |
|
||||
| `shopping_clothing` | `electronics` | Amazon, AMZN Mktp DE |
|
||||
| `housing` | `renovation_maintenance` | IKEA, Markus Festl, Granit, Sostrene Grene, Mol* tischdecken, Gaertnerei, Dehner |
|
||||
| `education` | `courses_college` | Kathrin Orlob (PEKiP), Nerina Aupperle |
|
||||
| `leisure` | `streaming` | WOW wowtv.de |
|
||||
| `financial_other` | `bank_fees` | Unidentified PayPal, Ratepay, N26 fees |
|
||||
| `Geschenke & Transfers` | *(income)* | One-off incoming transfers |
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Create subscriptions
|
||||
|
||||
```
|
||||
get_subscriptions_meta() ← get category_id and payment_method_id
|
||||
```
|
||||
|
||||
**Standard Cookie household subscriptions (as of 2026-07):**
|
||||
|
||||
| Name | Amount | billing_cycle | cycle_interval | category_id | payment_method_id |
|
||||
|---|---|---|---|---|---|
|
||||
| Miete | 1080.00 | monthly | 1 | 6 (Other) | 6 (Bank Transfer) |
|
||||
| Strom (SWM) | 79.00 | monthly | 1 | 3 (Utilities) | 6 |
|
||||
| Internet / TV / Telefon | 29.99 | monthly | 1 | 3 (Utilities) | 6 |
|
||||
| Hundefutter | 75.00 | monthly | 1 | 6 (Other) | 6 |
|
||||
| Netflix | 8.00 | monthly | 1 | 1 (Entertainment) | 6 |
|
||||
| Grover | 16.90 | monthly | 1 | 6 (Other) | 2 (Debit Card) |
|
||||
| Rundfunk ARD / ZDF | 55.08 | monthly | 3 | 1 (Entertainment) | 6 |
|
||||
| KuKita Daycare (Leon) | 503.00 | monthly | 1 | 5 (Education) | 6 |
|
||||
| Lillydoo diapers | 56.70 | monthly | 2 | 4 (Health) | 3 (PayPal) |
|
||||
|
||||
Monthly equivalent total: **1,838.60 €** (Yuvomi applies cycle_interval to prorate).
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Add recurring income entries
|
||||
|
||||
```
|
||||
stage_add_budget_entry(
|
||||
title="Kindergeld",
|
||||
amount=55.00,
|
||||
category="Sozialleistungen",
|
||||
date="YYYY-MM-01",
|
||||
is_recurring=True,
|
||||
recurrence_interval="monthly",
|
||||
)
|
||||
commit_pending(pending_id)
|
||||
```
|
||||
|
||||
**Standard recurring income:**
|
||||
|
||||
| Title | Amount | category |
|
||||
|---|---|---|
|
||||
| Kindergeld | +55.00 | Sozialleistungen |
|
||||
| Cookie Share | +2650.00 | Erwerbseinkommen *(see recommended amount below)* |
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Post variable transactions
|
||||
|
||||
For each non-skipped CSV row, call `stage_add_budget_entry` with the mapped
|
||||
category/subcategory and the actual transaction amount and date. Use the Partner
|
||||
Name + Payment Reference as the title (truncate to 100 chars).
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify
|
||||
|
||||
```
|
||||
get_budget_summary("YYYY-MM")
|
||||
list_subscriptions()
|
||||
```
|
||||
|
||||
Expected for a full month with KuKita:
|
||||
- Fixed expenses ≥ 1,838 € (subscriptions)
|
||||
- Variable expenses ≥ 500 € (groceries alone)
|
||||
|
||||
---
|
||||
|
||||
## Cookie Share: how much to transfer monthly
|
||||
|
||||
Calculated from Jan–Jun 2026 data (Cookie account, one-offs stripped):
|
||||
|
||||
| | €/month |
|
||||
|---|---|
|
||||
| **Fixed costs (subscriptions)** | **1,839** |
|
||||
| Miete | 1,080 |
|
||||
| KuKita *(permanent from Jul 2026)* | 503 |
|
||||
| Strom + SYNVIA + Rundfunk + Netflix + Grover + Hundefutter + Lillydoo | 256 |
|
||||
| **Variable (6-month averages)** | **1,032** |
|
||||
| Groceries | 595 |
|
||||
| Children products | 142 |
|
||||
| Dining & cafes | 100 |
|
||||
| Transport | 66 |
|
||||
| Drugstore | 52 |
|
||||
| Clothing, Amazon, Pharmacy | 77 |
|
||||
| **Total monthly spend** | **≈ 2,871** |
|
||||
| Minus Kindergeld (fixed income) | −55 |
|
||||
| Minus Pocket Money (conservative ~600 €) | −600 |
|
||||
| **→ Recommended Cookie Share** | **≈ 2,650 €** |
|
||||
| With 200 € buffer | **≈ 2,850 €** |
|
||||
|
||||
**Current Cookie Share (Jun 2026): 1,995 € — shortfall ~655 €.**
|
||||
|
||||
The gap was covered by irregular Pocket Money top-ups (avg 962 €/mo over 6 months, but
|
||||
highly variable: 121 €–3,000 €). KuKita starting in June is the biggest step-up; raising
|
||||
Cookie Share to **2,650 €** makes the budget self-sufficient without relying on top-ups.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-06-29 — Corrections from first real import
|
||||
- Subscriptions endpoint is `/budget/subscriptions`, NOT `/subscriptions/` (404).
|
||||
- `recurrence_rule` RRULE strings are rejected by the API; use `cycle_interval` instead.
|
||||
- `GET /budget/` (no filter) returns only non-recurring entries; use `?month=` for full view.
|
||||
- Added Cookie Share recommendation (2,650 €/month) based on 6-month expense analysis.
|
||||
- Added full category taxonomy table.
|
||||
|
||||
### 2026-06-29 — Initial runbook
|
||||
Created from Jan–Jun 2026 N26 Cookie account analysis.
|
||||
Reference in New Issue
Block a user