mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(moodlog): reverse-sync push HealthLog → MoodLog (v1.4.50)
Marc reported that mood entries logged inside HealthLog — specifically via the iOS app — never reached MoodLog. The historic integration polled MoodLog every 15 minutes and pulled new rows, but nothing flowed the other direction. iOS users who tracked mood in HealthLog ended up with one log per app and no overlap. `src/lib/moodlog/push.ts` adds `pushMoodEntriesToMoodLog(userId, entries)`. Best-effort reverse-sync push to MoodLog's new POST `/api/integrations/health-log/mood` endpoint (shipped in MoodLog v0.2.0). Same per-user moodLogUrlEncrypted + moodLogApiKeyEncrypted credentials the pull side uses, same isPublicUrl SSRF guard, same manual-redirect policy. Wraps every failure mode in a wide-event warning so a network blip or a stale MoodLog deploy can never bubble back to the user's mood-create request. `POST /api/mood-entries` (single) and `POST /api/mood-entries/bulk` now fire the push as a fire-and-forget side-effect after a successful create. Entries with source === "MOODLOG" skip inside the helper to avoid an echo loop. Bulk push only includes rows that landed as `inserted` — duplicates and skips don't retry a known failure. Pull side filters echoes: `syncMoodLogEntries` now skips entries where MoodLog reports `loggedVia: "HEALTHLOG"`. Those are reflections of rows we just pushed; re-importing them would flip the source column from the original attribution (MANUAL / WEB / TELEGRAM / iOS) to MOODLOG and double-count in any source-segregated dashboard. An `echoSkipped` wide-event annotation lets the operator confirm the round-trip is closing at one hop. Cross-repo: The matching MoodLog v0.2.0 release adds the inbound POST handler this push targets. Both apps must deploy together; MoodLog deploys first so HealthLog never hits a 405 / 404 on the new endpoint. Tests: 9 cases in src/lib/moodlog/__tests__/push.test.ts cover the MOODLOG-source filter, no-credentials skip, SSRF refusal, headers + body shape on the successful path, 5xx + network-reject + 3xx- redirect failure modes, and the tags JSON-string → array key normalisation. Pins the contract the call sites no longer await so future helper refactors can't silently break the fire-and-forget integration. HealthLog suite: 5279 pass, lint clean, typecheck clean.
This commit is contained in:
1 parent
49c9b425bb
commit
a39f3bf6af
7 files changed
+496
-1
No files matched your search
@@ -46,6 +46,7 @@ import {
|
||||
import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
|
||||
import { invalidateUserMood } from "@/lib/cache/invalidate";
|
||||
import { recomputeMoodBucketsForEntry } from "@/lib/rollups/mood-rollups";
|
||||
import { pushMoodEntriesToMoodLog } from "@/lib/moodlog/push";
|
||||
|
||||
const MAX_ENTRIES_PER_BATCH = 500;
|
||||
const BATCH_RATE_LIMIT_MAX = 60;
|
||||
@@ -282,6 +283,40 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
// v1.4.50 — reverse-sync the freshly-inserted batch to MoodLog so
|
||||
// an iOS one-shot backfill surfaces in both apps. Duplicates and
|
||||
// skips do NOT push (MoodLog already has them — or we'd just be
|
||||
// retrying a known failure). The helper filters MOODLOG-sourced
|
||||
// rows internally so a re-ingest of a MoodLog pull doesn't echo.
|
||||
if (inserted > 0) {
|
||||
const pushBatch: Array<{
|
||||
date: string;
|
||||
moodLoggedAt: Date;
|
||||
mood: string;
|
||||
note: string | null;
|
||||
tags: string | null;
|
||||
source: string;
|
||||
}> = [];
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
if (results[i]?.status !== "inserted") continue;
|
||||
const e = entries[i];
|
||||
const tz = user.timezone ?? DEFAULT_TIMEZONE;
|
||||
pushBatch.push({
|
||||
date: moodDateKey(e.moodLoggedAt, tz),
|
||||
moodLoggedAt: e.moodLoggedAt,
|
||||
mood: e.mood,
|
||||
note: e.note ?? null,
|
||||
tags: e.tags ? JSON.stringify(e.tags) : null,
|
||||
source: e.source,
|
||||
});
|
||||
}
|
||||
if (pushBatch.length > 0) {
|
||||
void pushMoodEntriesToMoodLog(user.id, pushBatch).catch(() => {
|
||||
/* helper wraps errors in wide-event warnings; defence-in-depth */
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return apiSuccess({
|
||||
processed: entries.length,
|
||||
inserted,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { withIdempotency } from "@/lib/idempotency";
|
||||
import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
|
||||
import { invalidateUserMood } from "@/lib/cache/invalidate";
|
||||
import { recomputeMoodBucketsForEntry } from "@/lib/rollups/mood-rollups";
|
||||
import { pushMoodEntriesToMoodLog } from "@/lib/moodlog/push";
|
||||
|
||||
function parseTags(tags: string | null): string[] {
|
||||
if (!tags) return [];
|
||||
@@ -198,6 +199,30 @@ async function postMoodEntry(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.4.50 — reverse-sync push to MoodLog. Fire-and-forget; the
|
||||
// helper itself is best-effort and never throws, so a 502 from
|
||||
// MoodLog or a transient network blip can never bubble back to
|
||||
// the user's create. The pull side (15-min cron) backfills any
|
||||
// entry that fails the push window. Entries with `source ===
|
||||
// "MOODLOG"` skip inside the helper to avoid an echo loop.
|
||||
void pushMoodEntriesToMoodLog(user.id, [
|
||||
{
|
||||
date: entry.date,
|
||||
moodLoggedAt: entry.moodLoggedAt,
|
||||
mood: entry.mood,
|
||||
note: entry.note ?? null,
|
||||
tags: entry.tags,
|
||||
source: entry.source,
|
||||
},
|
||||
]).catch(() => {
|
||||
// The helper already wraps its own errors in wide-event
|
||||
// warnings. The void + .catch is defence-in-depth so a
|
||||
// synchronous throw inside the helper's promise chain (e.g. a
|
||||
// future refactor that adds an await on a rejected promise
|
||||
// before the first try/catch) can't surface as an unhandled
|
||||
// rejection in the Next.js runtime.
|
||||
});
|
||||
|
||||
return apiSuccess({ ...entry, tags: parseTags(entry.tags) }, 201);
|
||||
} catch (err) {
|
||||
if (
|
||||
|
||||
Reference in new issue
Block a user