Initial: Mesh-Configs vom 03.09.2026 01:23
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
---
|
||||
name: pocketbase-deployment
|
||||
description: "PocketBase als Backend auf Linux-VPS deployen: Installation, systemd, Collection-Schema anlegen (0.25 Quirks!), Filter-Syntax, API-Integration mit Flask. Vermeidet die typischen 400/404-Fehler."
|
||||
version: 1.0.0
|
||||
author: mehlzoerwer
|
||||
platforms: [linux]
|
||||
---
|
||||
|
||||
# PocketBase Deployment (Linux VPS)
|
||||
|
||||
PocketBase als leichtgewichtiges Backend für Web-Projekte (PersoEnt, Weltklasse-OS). Getestet mit v0.25.0 auf Ubuntu 24.04.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd /tmp && curl -sL https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip -o pb.zip
|
||||
unzip -o pb.zip -d /opt/pocketbase
|
||||
/opt/pocketbase/pocketbase --version
|
||||
```
|
||||
|
||||
**systemd** (`/etc/systemd/system/pocketbase.service`):
|
||||
```ini
|
||||
[Unit]
|
||||
Description=PocketBase
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/pocketbase
|
||||
ExecStart=/opt/pocketbase/pocketbase serve --http=127.0.0.1:8090
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
```bash
|
||||
mkdir -p /opt/pocketbase/pb_data && chown -R www-data:www-data /opt/pocketbase
|
||||
systemctl daemon-reload && systemctl enable pocketbase && systemctl start pocketbase
|
||||
curl -s http://127.0.0.1:8090/api/health # {"message":"API is healthy."}
|
||||
```
|
||||
|
||||
## Superuser + Auth-Token
|
||||
|
||||
```bash
|
||||
# Superuser via CLI erstellen (API-Endpoint /_superusers existert NICHT zum Anlegen!)
|
||||
/opt/pocketbase/pocketbase superuser upsert admin@projekt.local P4ssw0rd!
|
||||
|
||||
# Token holen:
|
||||
TOKEN=$(curl -s -X POST http://127.0.0.1:8090/api/collections/_superusers/auth-with-password \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"identity":"admin@projekt.local","password":"P4ssw0rd!"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
|
||||
```
|
||||
|
||||
⚠️ **Token läuft lange** (exp ~1 Jahr bei `superuser upsert`), trotzdem bei API-Code hartcodieren nur für Prototypen. Produktion: OAuth/Middleware.
|
||||
|
||||
## 🔥 Collection-Schema anlegen (0.25 QUIRKS!)
|
||||
|
||||
**POST `/api/collections` mit `schema`-Array IGNORIERT das Schema komplett!** Die Collection wird OHNE Felder erstellt (nur `id`). Das führt zu 400 bei jedem Filter.
|
||||
|
||||
**Fix — Schema NACHTRÄGLICH via PATCH mit `id`-Feldern:**
|
||||
```bash
|
||||
curl -s -X PATCH http://127.0.0.1:8090/api/collections/health_records \
|
||||
-H "Authorization: $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"fields": [
|
||||
{"name":"weight","type":"number","required":true,"min":0,"max":500,"id":"w1","presentable":false,"system":false},
|
||||
{"name":"blood_pressure","type":"text","required":true,"min":3,"max":10,"id":"bp1","presentable":false,"system":false},
|
||||
{"name":"pulse","type":"number","required":true,"min":0,"max":300,"id":"p1","presentable":false,"system":false},
|
||||
{"name":"user_id","type":"text","required":true,"min":1,"max":100,"id":"u1","presentable":false,"system":false}
|
||||
]
|
||||
}'
|
||||
```
|
||||
→ Antwort zeigt `FIELDS: ['id', 'weight', 'blood_pressure', 'pulse', 'user_id']` ✅
|
||||
|
||||
**Jedes Feld BRAUCHT eine `id`!** Ohne `id` wird es abgelehnt.
|
||||
|
||||
## 🔥 Filter-Syntax (0.25)
|
||||
|
||||
| Falsch | Richtig | Warum |
|
||||
|--------|---------|-------|
|
||||
| `filter=user_id='x'` | `filter=user_id = "x"` | Leerzeichen + Doppelte Anführungszeichen |
|
||||
| `sort=-created` | `sort=-id` | `created` ist System-Feld → 400 bei Sort. **ABER `sort=-id` funktioniert (200):** PocketBase-IDs sind chronologisch aufsteigend, `-id` = neueste zuerst. VERIFIZIERT (2026-08-06): `sort=created` und `sort=-created` geben 400, `sort=-id` und `sort=weight` geben 200. Für "neueste zuerst" immer `sort=-id` nutzen. |
|
||||
|
||||
**Beispiel (funktioniert):**
|
||||
```
|
||||
GET /api/collections/health_records/records?filter=user_id%20%3D%20%22x%22&page=1&perPage=10
|
||||
```
|
||||
|
||||
**Flask-Integration (Backend):**
|
||||
```python
|
||||
import requests
|
||||
POCKETBASE_URL = "http://127.0.0.1:8090"
|
||||
PB_TOKEN = "<SUPERUSER_TOKEN>" # für Filter-Abfragen nötig (viewRule leer reicht nicht!)
|
||||
|
||||
# WRITE (POST):
|
||||
r = requests.post(f"{POCKETBASE_URL}/api/collections/health_records/records",
|
||||
json={"weight":72,"blood_pressure":"120/80","pulse":68,"user_id":"x"})
|
||||
|
||||
# READ (GET mit Filter):
|
||||
r = requests.get(f"{POCKETBASE_URL}/api/collections/health_records/records",
|
||||
params={"filter": 'user_id = "x"', "page":1, "perPage":10},
|
||||
headers={"Authorization": PB_TOKEN})
|
||||
```
|
||||
|
||||
⚠️ **`viewRule: ""` reicht NICHT für Filter-Queries** — PocketBase verlangt Auth-Header bei `filter=`. Admin-Token im Backend-Code löst das.
|
||||
|
||||
## 🔥 Write-Schutz: leerer `createRule`/`updateRule` blockt User-Writes (400)
|
||||
|
||||
**Symptom (2026-08-15):** PersoEnt-User mit `users/auth-with-password`-Token kann **lesen**, aber `POST /api/collections/health_records/records` → **`400 Failed to create record`**, selbst mit vollständigem Record, der exakt einem bestehenden entspricht.
|
||||
|
||||
**Ursache:** Die Collection hat **leere `createRule` UND `updateRule`** (= nur Superuser darf schreiben; normaler User darf nur bei gesetzter Regel). Das ist **gutes Sicherheits-Design**, aber man stolpert darüber, weil der User-Read funktioniert.
|
||||
|
||||
**Diagnose (definitiv, ohne Ratenein):**
|
||||
```bash
|
||||
# Collection-Regeln direkt aus der SQLite-DB lesen:
|
||||
python3 -c "
|
||||
import sqlite3
|
||||
c=sqlite3.connect('/opt/pocketbase/pb_data/data.db')
|
||||
for r in c.execute(\"SELECT name,createRule,updateRule FROM _collections WHERE name='health_records'\"):
|
||||
print('create:', repr(r[1]), '| update:', repr(r[2]))
|
||||
"
|
||||
# leere Regel = '' oder NULL → nur Superuser darf schreiben
|
||||
```
|
||||
|
||||
**Fix (Hauptweg):** Für Server-Integration mit **Superuser-Token** schreiben (nicht User-Token). Siehe `## Superuser + Auth-Token` oben — `pocketbase superuser upsert ...` anlegen + `collections/_superusers/auth-with-password`-Token verwenden. Behalte das restriktive Design bei (kein offener User-Write), statt die Regel zu lockern — sensible Gesundheitsdaten.
|
||||
|
||||
**Fallback (Option B, verifiziert 2026-08-15):** Wenn **KEIN Superuser-Passwort existiert** (Nutzer hat PB nicht aufgesetzt / kennt das Admin-PW nicht), kann der **Server-Integration-Poller direkt in die SQLite-DB schreiben** (root, läuft auf dem VPS):
|
||||
```python
|
||||
# direkter INSERT in /opt/pocketbase/pb_data/data.db — umgeht API-createRule komplett
|
||||
import sqlite3, shutil, datetime, os
|
||||
PB_DB = "/opt/pocketbase/pb_data/data.db"
|
||||
os.makedirs("/opt/pocketbase/pb_data/backups", exist_ok=True)
|
||||
# Rolling-Backup (max 3) VOR erstem Write — Backup-Regel
|
||||
backups = sorted(b for b in os.listdir("/opt/pocketbase/pb_data/backups") if b.endswith(".db"))
|
||||
if len(backups) >= 3: os.remove(os.path.join("/opt/pocketbase/pb_data/backups", backups[0]))
|
||||
shutil.copy2(PB_DB, f"/opt/pocketbase/pb_data/backups/data_{datetime.datetime.now():%Y%m%d_%H%M%S}.db")
|
||||
con = sqlite3.connect(PB_DB); cur = con.cursor()
|
||||
cols = [r[1] for r in cur.execute("PRAGMA table_info('health_records')")] # vorhandene Spalten pruefen
|
||||
vals = {"steps":981,"stress":22,"pulse":51,"sleep_hours":7.77,"user_id":"thorsten"}
|
||||
sub = [c for c in vals if c in cols] # NUR vorhandene Spalten senden
|
||||
cur.execute(f"INSERT INTO health_records ({','.join(sub)}) VALUES ({','.join('?'*len(sub))})",
|
||||
[vals[c] for c in sub]); con.commit(); con.close()
|
||||
```
|
||||
**PocketBase-Leseseite:** direkte Writes sind via API lesbar (`sort=-id`), weil PB die SQLite-DB direkt liest — aber PB-Validierung/Trigger werden umgangen. **Nur nutzen für** einfache, single-User-Inert-Tables (Cron-Poller auf `health_records`); **nicht** für Beziehungen/Migrationen/parallele PB-Writes. Vor dem ersten Write DOM-typ trotzdem: Collection-Spalten via `PRAGMA table_info` holen (kein `datum`/`source`-Feld annehmen — es kann fehlen). NICHTS erfinden, echte COL-Check.
|
||||
|
||||
## 🔥 Schema-Cache-Gotcha: per ALTER TABLE ergänzte Spalte wird NICHT von der API geliefert
|
||||
|
||||
**Symptom (2026-08-15):** Du fügst per direktem `ALTER TABLE health_records ADD COLUMN datum TEXT` eine neue Spalte hinzu und schreibst sie per Direkt-Write — aber die API liefert `datum` nicht (Record-Keys ohne das Feld), obwohl es in der SQLite-Tabelle existiert.
|
||||
|
||||
**Ursache:** PocketBase hält sein Collection-Schema **in `_collections.fields` (separate JSON) + im RAM-Cache**. Ein rohes `ALTER TABLE` ändert die Tabelle, aber **nicht das PB-Schema** → API white-/blacklistet das Feld nicht.
|
||||
|
||||
**Fix (verifiziert 2026-08-15):** Feld NACHträglich in PB-Schema eintragen UND Service neu laden:
|
||||
```bash
|
||||
# 1. Backup (Backup-Regel) — cp data.db data.db.bak-<ts>
|
||||
# 2. Feld in _collections.fields (JSON) eintragen:
|
||||
python3 -c "
|
||||
import sqlite3, json
|
||||
c=sqlite3.connect('/opt/pocketbase/pb_data/data.db')
|
||||
row=c.execute(\"SELECT fields FROM _collections WHERE name='health_records'\").fetchone()
|
||||
f=json.loads(row[0])
|
||||
if 'datum' not in [x.get('name') for x in f]:
|
||||
f.append({'name':'datum','type':'text','required':False,'system':False,'max':10,'min':0,'pattern':'','autogeneratePattern':'','primaryKey':False})
|
||||
c.execute(\"UPDATE _collections SET fields=? WHERE name='health_records'\",(json.dumps(f),)); c.commit()
|
||||
# 3. PocketBase-Service neu laden (RAM-Schema-Cache!):
|
||||
"
|
||||
systemctl restart pocketbase # ODER systemctl restart pocketbase.service
|
||||
# Verifikation: API-Response-keys enthalten jetzt das Feld
|
||||
```
|
||||
**Ohne den `systemctl restart pocketbase` liefert die API das neue Feld NICHT** — der RAM-Cache nimmt Schema-Änderungen erst nach Neustart an. Danach: `GET .../records?filter=...&sort=-id` → `datum` taucht in Record-Keys auf.
|
||||
|
||||
## ⌚ Wearable-/Geräte-Daten einspeisen (COROS-MCP-Beispiel)
|
||||
|
||||
## Flask + Nginx Reverse-Proxy
|
||||
|
||||
Backend unter `/var/www/<projekt>/backend/app.py` (systemd `persoent-api`, siehe `references/flask-deploy.md`). Nginx:
|
||||
```nginx
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:5000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Write test:
|
||||
curl -s -X POST https://<domain>/api/health/record -H 'Content-Type: application/json' \
|
||||
-d '{"weight":72,"blood_pressure":"120/80","pulse":68,"user_id":"test"}'
|
||||
# → {"id":"...","message":"Health record saved successfully"}
|
||||
|
||||
# Read test:
|
||||
curl -s "https://<domain>/api/health/records?user_id=test"
|
||||
# → {"records":[...],"total":1}
|
||||
```
|
||||
|
||||
## Referenzen
|
||||
- `references/flask-deploy.md` — Flask-App als systemd-Service + Nginx Proxy (PersoEnt-Pattern)
|
||||
- `references/pocketbase-quirks-19-07-2026.md` — Session-Transcript der 400-Fehler + Fixes
|
||||
- `references/coros-mcp-wearable-integration.md` — COROS-Sportuhr-Daten via offiziellem `coros-mcp` CLI in health_records (Login-Flow, korrekte `--tool`/`--arguments-json`-Syntax, Text-Block-Parsing) für Wearable-/Geräte-Einspeisung
|
||||
@@ -0,0 +1,65 @@
|
||||
# COROS-MCP Integration (offiziell, Wearable-Daten → PocketBase)
|
||||
|
||||
Muster für das Einspeisen von COROS-Sportuhr-Daten in eine PocketBase-`health_records`-Collection,
|
||||
als Ergänzung zu `pocketbase-deployment/SKILL.md`. Verifiziert 2026-08-15.
|
||||
|
||||
## Install & Login (offizielles Paket, von COROS)
|
||||
|
||||
```bash
|
||||
npm install -g coros-mcp # offizielles Paket (coros_openplatform)
|
||||
coros-mcp --help # commands: login, login-start/-finish/-status, list-tools, describe-tool, call-tool, apply-openclaw
|
||||
```
|
||||
|
||||
**Login-Flow (nur das COROS-Konto des Nutzers kann das):**
|
||||
```bash
|
||||
coros-mcp login-start # gibt einen Browser-Login-Link aus (z.B. https://mcpeu.coros.com/cli/login/...)
|
||||
# Nutzer öffnet Link im Browser + meldet sich mit COROS-Konto an
|
||||
coros-mcp login-finish # "login ok" — bestätigt Session
|
||||
coros-mcp list-tools # zeigt verfügbare Tools (JSON)
|
||||
```
|
||||
Token wird in `~/.coros-mcp-skill-gateway-ts/<region>/token.json` gecacht (root-only machen: `chmod 600/700`).
|
||||
|
||||
## 🔥 Korrekte CLI-Syntax (Nicht raten!)
|
||||
|
||||
`call-tool`/`describe-tool` erwarten Flags, NICHT nur den Namen als Positional:
|
||||
```bash
|
||||
# FALSCH (gibt "unknown argument"):
|
||||
coros-mcp call-tool queryDailyHealthData
|
||||
# RICHTIG (--tool + --arguments-json):
|
||||
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{}'
|
||||
coros-mcp describe-tool --tool queryDailyHealthData
|
||||
```
|
||||
Das `--help` auf Unterbefehlen zeigt nur die Haupt-Hilfe; die Flags stehen im installierten
|
||||
Paket (`$(npm root -g)/coros-mcp/dist/*.js`, `call-tool requires --tool`, `--arguments-json`).
|
||||
|
||||
## Wertvollste Tools für Gesundheits-Detektiv
|
||||
|
||||
- `queryDailyHealthData` — 7-Tage-Health (Steps, Stress, Calories, Sleep-Übersicht je Tag)
|
||||
- `querySleepData` / `querySleepHrv` — Schlaf + HRV (P0-Merkmale)
|
||||
- `queryStressLevel` / `queryStressTimeSeries` — Stress
|
||||
- `queryAvgHeartRate` / `queryRestingHeartRate` — Puls
|
||||
- `querySportRecords` / `getActivityDetail` / `downloadActivityFitFiles` — Trainingsdaten
|
||||
- `queryRecoveryStatus` / `queryFitnessAssessmentOverview` — Erholung % + VO2max/Threshold-Pace
|
||||
|
||||
## Text-Block-Parsing (wichtige Heuristik)
|
||||
|
||||
COROS-`call-tool`-Antworten sind **Text**, mit einem 7-Tage-Block je Datum:
|
||||
```
|
||||
--- 20260815 ---
|
||||
Steps: 981 | Calories: 47 kcal
|
||||
Stress: Avg 22
|
||||
Sleep Summary: Total: 7h 46min | Deep: ...
|
||||
```
|
||||
**Pitfall:** Ein simpler "nimm erste Zahl nach Label" greift das **Datum** statt Steps (→ steps=20260815).
|
||||
**Robust:** `re.split(r'--- (\d{8}) ---', text)` → letzten Tagesblock nehmen, dort Werte ziehen.
|
||||
Umlaute-fest: `get_num(body, "Stress")` mit `re.search(re.escape(label)+r'[^\d]*?([\d.,]+)')`
|
||||
umgeht Zwischenwörter wie "Avg" (also `Stress: Avg 22` → 22).
|
||||
Datum: `'20260815'` → `'2026-08-15'` formatieren.
|
||||
|
||||
## In PocketBase schreiben
|
||||
|
||||
Direkter SQLite-Write (root) umgeht das leere `createRule`-Problem — siehe SKILL.md
|
||||
"Write-Schutz: leerer createRule blockt User-Writes". Beim INSERT **`PRAGMA table_info`** prüfen,
|
||||
welche Spalten existieren, und nur vorhandene senden (kein `datum`/`source` annehmen).
|
||||
|
||||
Cron: `0 3 * * * cd /opt/persoent_api/coros && python3 coros-persoent-poller.py >> /var/log/coros-poller.log 2>&1`
|
||||
@@ -0,0 +1,55 @@
|
||||
# Flask Deploy Pattern (PersoEnt, 19.07.2026)
|
||||
|
||||
## venv anlegen (Ubuntu 24.04)
|
||||
```bash
|
||||
apt-get install -y python3.12-venv
|
||||
python3 -m venv /var/www/<projekt>/backend/venv
|
||||
/var/www/<projekt>/backend/venv/bin/pip install flask flask-cors requests
|
||||
```
|
||||
|
||||
## systemd Service
|
||||
`/etc/systemd/system/<projekt>-api.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=<Projekt> API (Flask)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/<projekt>/backend
|
||||
Environment=PATH=/var/www/<projekt>/backend/venv/bin
|
||||
ExecStart=/var/www/<projekt>/backend/venv/bin/python app.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Kritisch: Berechtigungen nach jedem File-Edit!
|
||||
`patch`/`write_file`/`sed` ändern den Owner → `chown www-data:www-data app.py` VOR `systemctl restart`!
|
||||
Sonst: `Permission denied` + Restart-Loop.
|
||||
|
||||
## Nginx (in sites-enabled/<domain>)
|
||||
```nginx
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:5000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
`nginx -t && systemctl reload nginx`
|
||||
|
||||
## Token in Code schreiben (NICHT via sed!)
|
||||
⚠️ `sed -i "s|^PB_TOKEN = .*|PB_TOKEN = \"$TOKEN\"|"` schneidet langen Token ab (Terminal kürzt bei Display, aber das Problem: sed ersetzt mit gekürztem Wert wenn Variable im Heredoc/SSH-Kontext kaputt ist).
|
||||
**Sicher:** Python-Script nutzen:
|
||||
```python
|
||||
import re
|
||||
token = "VOLLER_TOKEN"
|
||||
with open('app.py') as f: c = f.read()
|
||||
c = re.sub(r'PB_TOKEN = ".*"', f'PB_TOKEN = "{token}"', c)
|
||||
with open('app.py','w') as f: f.write(c)
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# PocketBase 0.25 Quirks — Session Transcript (19.07.2026)
|
||||
|
||||
## Fehler 1: Collection ohne Felder
|
||||
POST `/api/collections` mit `schema:[...]` → 200, aber Collection hat NUR `id`-Feld.
|
||||
Filter/Abfragen schlagen fehl (400).
|
||||
**Fix:** PATCH mit `fields:[{name,type,id,...}]` — jedes Feld braucht `id`.
|
||||
|
||||
## Fehler 2: Filter 400
|
||||
`filter=user_id='x'` → 400
|
||||
`filter=user_id = "x"` → 200 ✅ (Leerzeichen + Double-Quotes)
|
||||
|
||||
## Fehler 3: sort=-created → 400
|
||||
`?sort=-created` am Ende der Query → 400.
|
||||
`created` ist System-Feld in 0.25, Sortierung darüber nicht erlaubt ohne Index.
|
||||
**Fix:** `sort` komplett weglassen.
|
||||
|
||||
## Fehler 4: viewRule="" reicht nicht für Filter
|
||||
GET ohne Auth-Header → 400 bei `filter=`.
|
||||
**Fix:** Admin-Token im Backend-Code (`headers={"Authorization": PB_TOKEN}`).
|
||||
|
||||
## Verifikation (End-to-End)
|
||||
```
|
||||
POST /api/health/record {"weight":73.2,"blood_pressure":"118/76","pulse":64,"user_id":"e2e_test"}
|
||||
→ {"id":"...","message":"Health record saved successfully"}
|
||||
|
||||
GET /api/health/records?user_id=e2e_test
|
||||
→ {"records":[{...}],"total":1}
|
||||
```
|
||||
|
||||
## Token-Hardcoding Gotcha
|
||||
`sed` hat Token auf `eyJhbG...4TQA` (23 Zeichen statt 224) gekürzt → 400 blieb.
|
||||
Python `re.sub` mit vollem Token string gelöst.
|
||||
Reference in New Issue
Block a user