Files
mesh-configs/skills/devops/coros-mcp-integration/references/coros-mcp-backfill-parsing-pitfalls.md
T

4.5 KiB

COROS-MCP — Historie/Backfill + Parser-Pitfalls (verifiziert 2026-08-18)

Ergänzt zu coros-mcp-data-shapes.md. Alle Befunde sind reale call-tool-Antworten, nicht geraten.

Health-Historie: days-Parameter + Server-Limit

queryDailyHealthData akzeptiert im Schema einen days-Parameter (integer, default 7).

coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{"days":30}'
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{"days":365}'

WICHTIG (verifiziert 2026-08-18): Egal ob days:30, days:90 oder days:365 — die COROS-API liefert maximal ~53 Health-Tage zurück (z.B. nur 2026-05-16 .. 2026-08-15, 53 Blöcke). Das ist ein SERVER-Limit von COROS, kein Parser- oder Client-Fehler. Mehr Health-Historie gibt es über diese API nicht; für längere Geschichte bleibt nur der Sport/FIT-Pfad.

Auch bei "alles was geht" lohnt kein Versuch über 53 hinaus zu holen — wiederhole sportBackfill dafür nicht.

Sport-Historie: querySportRecords mit Datumsbereich

querySportRecords akzeptiert (anders als Health) startDate/endDate im Format yyyyMMdd (kein days) und sportTypeCodes-Array:

coros-mcp call-tool --tool querySportRecords --arguments-json \
  '{"startDate":"20250501","endDate":"20260815","sportTypeCodes":[65535]}'
  • sportTypeCodes: [65535] = alle Sportarten (sonst numerische Codes, nie Namen).
  • Sport-Historie reicht deutlich weiter zurück als Health (1 Jahr+). 20 Records über 15 Monate bestätigt.
  • Antwort enthält je Workout: N. <Sportart> — YYYY-MM-DD, Time Window: startTimestamp=.. | endTimestamp=.., Duration, Avg HR: <n> bpm, Calories: <n> kcal, Distance: <x> km, LabelId: <Zahl>, SportType: <code>.

🔥 PITFALL: COROS-content[0].text enthält LITERALES \n, KEINE echten Newlines

(Wichtigster Fund — ohne Normalisierung matcht nichts.)

querySportRecords (und teils andere) liefern im Text literale 2-Zeichen-Sequenz \n (ein Backslash + n), keine echten Zeilenumbrüche. splitlines() / repr() in einem Launch zeigen das nicht zuverlässig.

Debug-Befund (2026-08-18):

text = d['content'][0]['text']
'\n' in text   # False  (kein echter Newline)
'\\n' in text  # True   (literales Backslash-n vorhanden)

Fix — normalisiere VOR jedem Zeilen-Parsing:

text = text.replace('\\n', '\n')
lines = text.splitlines()   # erst jetzt funktioniert splitlines()

Übersehen → alle Regex gegen splitlines() finden 0 Zeilen (kein Fehler, nur 0 Treffer). Achte auf die richtige Backslash-Tiefe: Ersetze das 1-Backslash-\n-Literal, nicht doppel.

⏱️ Dauer-Format ist uneinheitlich: HH:MM:SS vs MM:SS vs SS

COROS-Duration:-Werte kommen gemischt:

  • 1:00:14 → 1h = 60 min (3 Segmente)
  • 2:03:36 → 2h03m = 123 min (3 Segmente)
  • 51:35 → 51 min (2 Segmente — NICHT 51std35!)
  • 0:22 → 22 Sek → 0 min (2 Segmente)

Fehlerhafte Regex (\d+):(\d+)(?::(\d+))? macht 51:35 fälschlich zu 51*60+35 = 3095 min (absurd 63h). Robust:

m = re.search(r'Duration:\s*([\d:]+)', block)
seg = [int(x) for x in m.group(1).split(':')]
if len(seg) == 3:      # HH:MM:SS
    dauer_min = seg[0]*60 + seg[1]
elif len(seg) == 2:    # MM:SS
    dauer_min = seg[0]
else:                  # SS
    dauer_min = seg[0] // 60

Sport-Daten → SEPARATE Tabelle (nicht health_records)

Ein Tag kann mehrere Workouts haben → passt NICHT in health_records (1 Zeile/Tag). Eigene Tabelle:

CREATE TABLE IF NOT EXISTS sport_records (
    label_id  TEXT PRIMARY KEY,     -- COROS-LabelId je Workout (natürlicher PK)
    datum     TEXT NOT NULL,        -- yyyyMMdd
    sportart  TEXT NOT NULL,
    sport_type_code INTEGER,        -- COROS SportType-Code
    dauer_min INTEGER,
    avg_hr    INTEGER,
    kalorien  INTEGER,
    distanz_km REAL,
    created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_sport_datum ON sport_records(datum);

Idempotent: INSERT OR IGNORE ... ON CONFLICT(label_id) DO NOTHING — Workouts sind nach Import unveränderlich, kein UPDATE nötig. (Direkter DB-Write, da health_records-API createRule leer = nur Superuser schreibt; siehe pocketbase-deployment.)

Reihenfolge bei Backfill in bestehende DB

  1. Wenn schon fehlerhafte Records mit dem alten Dauer-Parser geschrieben wurden → Flushing die Tabelle (DELETE FROM sport_records) oder per label_id neu überschreiben, DANN Neu-Load mit korrektem Parser.
  2. Immer vorher DB-Backup (Backup-Regel): cp /opt/pocketbase/pb_data/data.db data.db.bak-<ts>.