mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
The runbook for diagnosing why pending changes aren't flushing on
mana.how. Was sitting untracked in the repo root for the last week
of debugging; committing now that the debug surface it depends on
(window.__unifiedSync + getDebugInfo() + the surfaced silent
failures) actually exists.
Three steps in dependency order:
Schritt A — read _pendingChanges directly from IndexedDB to find
out which appIds and collections are stuck. Output drives the
appId choice for Schritt B.
Schritt B — manual POST against /sync/{appId} with the JWT from
localStorage. Status code mapping table tells you whether the
bug is server-side (4xx/5xx) or client-side (200 → sync engine
isn't running).
Schritt C — read window.__unifiedSync.getDebugInfo() (newly
exposed in this commit batch) to see channel state. Compare
knownAppIds against the Schritt A output: any appId with pending
rows but no channel will accumulate forever, and the new
console.warn from sync.ts will already be naming it explicitly.
Schritt B is the diagnostic key — everything else follows from
its status code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
6 KiB
Markdown
157 lines
6 KiB
Markdown
# Sync-Debug auf mana.how
|
|
|
|
Ziel: rausfinden warum die 13 Pending-Changes nicht syncen.
|
|
|
|
## Bisheriger Stand
|
|
|
|
✅ **Schon bestätigt** (musst du nicht nochmal laufen lassen):
|
|
|
|
- `SYNC_URL = https://sync.mana.how` ✓
|
|
- LocalStorage hat `@auth/appToken` (454 Zeichen JWT, EdDSA, `kid: xQMrnC5JoHYGDRVlFQMUVdp9iBLPV4Tz`) und `@auth/refreshToken` (32-Zeichen-Opak)
|
|
- Der `kid` matcht den aktuellen Prod-JWKS — also kein Signing-Drift
|
|
- `sync.mana.how` ist erreichbar, Cloudflare → mana-core-sync funktioniert
|
|
- mana-core-sync Container ist healthy, DB existiert, Auth-Middleware antwortet
|
|
|
|
❓ **Offen:**
|
|
|
|
1. Welche Tabellen / appIds hängen tatsächlich (Schritt A)
|
|
2. Akzeptiert der Server unseren echten JWT (Schritt B)
|
|
3. Läuft der Client-seitige Sync-Channel überhaupt (Schritt C)
|
|
|
|
---
|
|
|
|
## Schritt A — Pending-Changes inspizieren
|
|
|
|
```js
|
|
(() => {
|
|
const req = indexedDB.open('mana');
|
|
req.onsuccess = () => {
|
|
const db = req.result;
|
|
if (!db.objectStoreNames.contains('_pendingChanges')) {
|
|
console.log('Kein _pendingChanges store. Vorhandene stores:', [...db.objectStoreNames]);
|
|
return;
|
|
}
|
|
const tx = db.transaction('_pendingChanges', 'readonly');
|
|
const all = tx.objectStore('_pendingChanges').getAll();
|
|
all.onsuccess = () => {
|
|
const pending = all.result;
|
|
const byApp = pending.reduce((a, p) => {
|
|
a[p.appId] = (a[p.appId] || 0) + 1;
|
|
return a;
|
|
}, {});
|
|
const byTable = pending.reduce((a, p) => {
|
|
a[p.collection] = (a[p.collection] || 0) + 1;
|
|
return a;
|
|
}, {});
|
|
console.log('Total pending:', pending.length);
|
|
console.log('By appId:', byApp);
|
|
console.log('By collection:', byTable);
|
|
console.log('Oldest:', pending[0]);
|
|
console.log('Newest:', pending[pending.length - 1]);
|
|
console.log('All:', pending);
|
|
};
|
|
all.onerror = () => console.error('getAll error', all.error);
|
|
};
|
|
req.onerror = () => console.error('open error', req.error);
|
|
})();
|
|
```
|
|
|
|
Output: gib mir die Zeile `By appId:` — die brauche ich für Schritt B.
|
|
|
|
---
|
|
|
|
## Schritt B — Manueller Push mit dem echten JWT
|
|
|
|
Ersetz `APPID_HIER` durch eine appId aus Schritt A (z.B. `todo`, `photos`, `memoro`, `times`).
|
|
|
|
```js
|
|
(async () => {
|
|
const url = window.__PUBLIC_SYNC_SERVER_URL__;
|
|
const token = localStorage.getItem('@auth/appToken');
|
|
console.log('Token segments:', token?.split('.').length, '(JWT muss 3 sein)');
|
|
console.log('Token first 40:', token?.slice(0, 40));
|
|
|
|
// Decode JWT payload um aud/iss/exp/sub zu sehen:
|
|
try {
|
|
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
console.log('JWT payload:', payload);
|
|
console.log(
|
|
'expired?',
|
|
payload.exp * 1000 < Date.now(),
|
|
'exp:',
|
|
new Date(payload.exp * 1000).toISOString()
|
|
);
|
|
} catch (e) {
|
|
console.error('JWT decode failed:', e);
|
|
}
|
|
|
|
const res = await fetch(`${url}/sync/APPID_HIER`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
'X-Client-Id': 'debug-probe',
|
|
},
|
|
body: JSON.stringify({ clientId: 'debug-probe', since: '', changes: [] }),
|
|
});
|
|
console.log('Status:', res.status);
|
|
console.log('Body:', await res.text());
|
|
})();
|
|
```
|
|
|
|
Was die Antwort bedeutet:
|
|
|
|
| Status | Diagnose |
|
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `200` | Server akzeptiert Token + Body → Bug ist **im Client**: Sync-Loop läuft nicht oder der auth-Store reicht den Token nicht weiter |
|
|
| `401` | JWT-Validierung scheitert. Payload aus dem `atob()`-Decode sagt warum: `aud` mismatch, `exp` abgelaufen, `iss` falsch |
|
|
| `403` | Token gültig, aber RLS / appId-Mapping verweigert Zugriff |
|
|
| `400` | Auth ok, aber Body-Schema unerwartet → Protokoll-Drift Client↔Server |
|
|
| `5xx` | Server-Bug |
|
|
| Network error / CORS | URL/CORS kaputt (unwahrscheinlich, curl-Probe hat funktioniert) |
|
|
|
|
---
|
|
|
|
## Schritt C — Client-seitiger Sync-Channel-State
|
|
|
|
```js
|
|
(() => {
|
|
const u = window.__unifiedSync;
|
|
if (!u) {
|
|
console.log(
|
|
'window.__unifiedSync ist nicht gesetzt. unifiedSync wurde nie initialisiert (vermutlich ist authStore.isAuthenticated = false beim Mount des (app)-Layouts).'
|
|
);
|
|
return;
|
|
}
|
|
console.log('Debug info:', u.getDebugInfo());
|
|
console.log('Status:', u.status, 'online:', u.online);
|
|
})();
|
|
```
|
|
|
|
`getDebugInfo()` liefert pro Channel:
|
|
|
|
- `appId`, `tables` (welche Dexie-Tabellen syncen)
|
|
- `lastError` (letzter Fehler — wenn `null`, lief noch nichts schief)
|
|
- `hasPushTimer` / `hasPullTimer` (ob ein Timer aktiv ist)
|
|
- `knownAppIds` als Top-Level — vergleich mit `byApp` aus Schritt A: jede appId aus Schritt A muss in `knownAppIds` enthalten sein, sonst werden ihre pending changes silently nie pushen.
|
|
|
|
Außerdem: ab dieser Sync-Patch-Version logged der Push silently failures als `console.warn`:
|
|
|
|
- `[mana-sync] push: no channel registered for appId="..."` → Schreib-/Registry-Drift, Schritt A appId fehlt im SYNC_APP_MAP
|
|
- `[mana-sync] push[X]: getToken() returned null` → Token-Refresh ist gescheitert
|
|
|
|
Wenn `__unifiedSync` nicht auf `window` hängt, zeig mir zusätzlich das aus der **Network**-Tab:
|
|
|
|
1. Filter `sync.mana.how`
|
|
2. Reload (Cmd+R)
|
|
3. Screenshot — was sehen wir? Requests? Welcher Status?
|
|
|
|
---
|
|
|
|
## Wichtigste Reihenfolge
|
|
|
|
1. **Schritt A** zuerst → sagt uns welche appIds betroffen sind
|
|
2. **Schritt B** mit einer dieser appIds → entscheidet ob Bug Server- oder Client-seitig ist
|
|
3. **Schritt C** nur wenn Schritt B `200` zurückgibt (= Bug ist client-seitig)
|
|
|
|
Schritt B ist der diagnostische Schlüssel — alles andere folgt aus seinem Status-Code.
|