mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18: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>
6 KiB
6 KiB
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
kidmatcht den aktuellen Prod-JWKS — also kein Signing-Drift sync.mana.howist erreichbar, Cloudflare → mana-core-sync funktioniert- mana-core-sync Container ist healthy, DB existiert, Auth-Middleware antwortet
❓ Offen:
- Welche Tabellen / appIds hängen tatsächlich (Schritt A)
- Akzeptiert der Server unseren echten JWT (Schritt B)
- Läuft der Client-seitige Sync-Channel überhaupt (Schritt C)
Schritt A — Pending-Changes inspizieren
(() => {
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).
(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
(() => {
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 — wennnull, lief noch nichts schief)hasPushTimer/hasPullTimer(ob ein Timer aktiv ist)knownAppIdsals Top-Level — vergleich mitbyAppaus Schritt A: jede appId aus Schritt A muss inknownAppIdsenthalten 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:
- Filter
sync.mana.how - Reload (Cmd+R)
- Screenshot — was sehen wir? Requests? Welcher Status?
Wichtigste Reihenfolge
- Schritt A zuerst → sagt uns welche appIds betroffen sind
- Schritt B mit einer dieser appIds → entscheidet ob Bug Server- oder Client-seitig ist
- Schritt C nur wenn Schritt B
200zurückgibt (= Bug ist client-seitig)
Schritt B ist der diagnostische Schlüssel — alles andere folgt aus seinem Status-Code.