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
|
||||
Reference in New Issue
Block a user