Skip to content

fix: batch API endpoint exports for large history imports#86

Merged
CodyBontecou merged 15 commits into
CodyBontecou:mainfrom
Just-Insane:fix/batch-api-endpoint-exports
Jul 20, 2026
Merged

fix: batch API endpoint exports for large history imports#86
CodyBontecou merged 15 commits into
CodyBontecou:mainfrom
Just-Insane:fix/batch-api-endpoint-exports

Conversation

@Just-Insane

Copy link
Copy Markdown
Contributor

Closes #85.

Problem

API Endpoint export built one healthmd.api_export JSON envelope for the entire selected date range and POSTed it in a single request. Large historical exports (e.g. ~7 months of granular HealthKit data) could exceed Cloudflare's request limits or time out.

Change

APIEndpointExportRunner.export now splits the normalized, sorted date range into bounded sequential batches before uploading:

  • New internal constant APIEndpointExportRunner.defaultMaxBatchDaySpan = 7 (calendar days), passed as a default maxBatchDaySpan parameter so it can later become user-configurable without touching the batching algorithm.
  • Dates are chunked into consecutive groups of at most maxBatchDaySpan days.
  • Each batch fetches its own records/failed-date-details/external records, then calls the injected upload closure once per batch with that batch's own date_range.start, date_range.end, records, and failed-date details. APIExportClient required no changes — it already builds the envelope from whatever range/records it's given, so each batch produces a correctly-scoped envelope automatically.
  • Batches upload strictly serially (awaited one at a time, no concurrency).
  • If a batch's upload throws, the run stops immediately: no later batches are attempted. A FailedDateDetail is appended describing the failed date range (yyyy-MM-dd to yyyy-MM-dd) and the underlying error's localizedDescription — never the health payload or the Authorization header value, since the existing error types never carry those.
  • successCount accumulates per successfully-uploaded batch, so a later failure does not erase credit for earlier successful batches.
  • A batch with no exportable records (e.g. all dates in that window had no data) is skipped without aborting the rest of the run — mirroring the existing single-batch "no data that day" behavior.
  • Small/normal exports and the scheduled two-day lookback naturally resolve to a single batch, so their behavior is unchanged.

No server-side protocol changes; the wire format per request is identical to before, just scoped to at most 7 days per request.

Tests

Added to HealthMdTests/Managers/APIEndpointExportRunnerTests.swift:

  • testFifteenDayRangeSplitsIntoThreeSequentialSevenDayBatches — 15 consecutive days produce exactly 3 upload calls with 7, 7, and 1 day(s) respectively; asserts each call's date range and record set matches its batch.
  • testBatchFailureStopsSubsequentBatchesAndPreservesPriorSuccess — forces the second of three batches to throw; asserts the third batch's upload closure is never invoked, successCount reflects only the first (successful) batch, and a .fileWriteError detail mentioning the failure is present.
  • testEmptyDatesReturnsZeroCounts and testSingleDayWithNoDataReportsNoHealthDataAndSkipsUpload — explicit coverage that empty-input and single-day/no-data behavior is unchanged, alongside the pre-existing testUploadsOnlyDatesWithDataAndReportsEmptyDates, testProviderOnlyDayRemainsSupplementalAndDoesNotTriggerUpload, and testUploadFailureReturnsFileWriteFailureWithoutCountingPreparedRecordsAsSuccess, all still passing unmodified.

Test command:

xcodebuild test \
  -project HealthMd.xcodeproj \
  -scheme HealthMd \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:HealthMdTests/APIEndpointExportRunnerTests

I don't have a macOS/Xcode toolchain in this environment to execute the build, so I have not run this locally — please run the target and confirm before merging. All new/edited tests only exercise the injected fetchHealthData/upload closures (no HealthKit or network access), so they should run in a standard simulator/CI job without special entitlements.

Docs

Updated docs/features/api-endpoint-export.md:

  • "Practical limits" now describes automatic sequential batching (7-day default) and per-batch envelope scoping.
  • Notes the endpoint may safely receive overlapping/re-sent exports because Health.md's reference receiver behavior retains the latest revision per day — not a claim that Health.md deduplicates arbitrary raw files.
  • Troubleshooting table gets a row for a run stopping partway through a large range.
  • "Implementation notes" updated to describe the batching responsibility split between APIEndpointExportRunner and APIExportClient.

Scope

Only HealthMd/Shared/Managers/APIEndpointExportRunner.swift, its test file, and the feature doc changed. APIExportClient.swift is untouched — it already accepted per-call ranges/records, which is what made per-batch envelopes possible without protocol changes.

Split selected API export date ranges into bounded sequential
batches (7 calendar days by default) instead of uploading one
oversized envelope. Batches upload serially; the first failed
batch stops the run and reports its date range while preserving
success accounting from prior batches. Small exports and the
scheduled two-day lookback are unaffected (single batch).
@Just-Insane
Just-Insane marked this pull request as ready for review July 17, 2026 20:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 244fec2af8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +117 to +120
let batchSpan = max(1, maxBatchDaySpan)
let batches: [[Date]] = stride(from: 0, to: normalizedDates.count, by: batchSpan).map {
Array(normalizedDates[$0..<min($0 + batchSpan, normalizedDates.count)])
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route manual API exports through the batch runner

The batching is not used by the manual history-export flow that needs it: the only production caller of APIEndpointExportRunner.export is the scheduled path at HealthMd/iOS/SchedulingManager.swift:698, while exportDataToAPIEndpoint still accumulates the entire selected range and calls APIExportClient.upload once at HealthMd/iOS/ContentView.swift:951. Consequently, selecting a large historical range in the app still creates the original oversized request and can hit the same timeout or request-size limit this change is intended to fix.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks — this was the real gap. ContentView.exportDataToAPIEndpoint now calls APIEndpointExportRunner.export instead of duplicating the fetch loop and calling APIExportClient.upload once directly. Added an onProgress callback to the runner so the manual UI keeps its per-date progress bar across what may now be several batch uploads. See 32c3ecd/d765f9b.

Comment on lines 204 to 208
allFailedDateDetails.append(FailedDateDetail(
date: batchStart,
reason: .fileWriteError,
errorDetails: "Batch upload failed for \(rangeDescription(start: batchStart, end: batchEnd)): \(error.localizedDescription)"
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track every date skipped after a failed batch

When a later batch upload fails, this records only batchStart and then returns, so the other records in that failed batch and every date in subsequent unattempted batches are absent from failedDateDetails. For example, if batch 2 of a 15-day export fails, the result reports 7 successes and only day 8 as failed even though days 9–15 were not delivered; consumers such as the failed-date history/retry UI operate on this array and therefore cannot identify all missing dates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — on a batch upload failure we now record a FailedDateDetail for every date whose record was in that failed batch, plus every date in every batch after it (marked as "not attempted" rather than "failed", since they were never sent). Added testBatchFailureStopsSubsequentBatchesAndPreservesPriorSuccess assertions covering all 8 remaining dates (batch start through the untried trailing batch).

Comment on lines +182 to +185
guard !batchRecords.isEmpty else {
// Nothing to upload for this batch; continue to the next one
// so a single sparse batch doesn't abort the whole range.
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve failure-only batches at the endpoint

When every date in one batch has no readable data or a fetch error but a later batch contains records, this guard drops the first batch without sending its failed_date_details anywhere. Previously, a mixed range with at least one record produced one envelope containing failures for all other requested dates; now a scheduled lookback spanning multiple batches can leave the receiver unable to distinguish those dates from dates that were never requested. Send a failure-only envelope or otherwise carry these details to the endpoint without losing their range.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by carrying a failure-only batch's failed_date_details forward and attaching them to the next batch that does upload successfully, so the endpoint still receives them (matching the pre-batching single-envelope behavior). If no later batch succeeds, they're flushed via a dedicated failure-only upload call at the end — but only if at least one batch succeeded overall, so an entirely-empty range still sends nothing (unchanged prior behavior). Covered by new tests testFailureOnlyBatchIsCarriedForwardToNextSuccessfulUpload and testTrailingFailureOnlyBatchIsFlushedInDedicatedUpload.

Comment on lines +179 to +180
allFailedDateDetails.append(contentsOf: batchFailedDateDetails)
totalExternalRecordCount += batchExternalRecords.count

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count external records only after their batch uploads

This increments totalExternalRecordCount before the upload succeeds, so when a later batch is rejected the returned partial-success result includes external records from that undelivered batch. ExportResult.totalFilesWritten incorporates this field and scheduled exports persist that value through ExportOrchestrator.recordResult, causing history/accounting to report provider sidecars as exported even though the endpoint never received them; move the increment to the successful-upload path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — totalExternalRecordCount is now only incremented inside the successful-upload branch, not when the records are fetched. Added testExternalRecordCountOnlyIncludesSuccessfullyUploadedBatches (single-day batches, second batch rejected) asserting externalRecordFileCount reflects only the delivered batch.

…counting

Addresses review feedback on CodyBontecou#86:

- Route the manual export flow (ContentView.exportDataToAPIEndpoint) through
  APIEndpointExportRunner.export instead of duplicating the fetch/upload
  loop and calling APIExportClient directly. This was the actual production
  bug this PR needed to fix -- the scheduled path already used the runner,
  but manual history exports (the common case for large ranges) still built
  one oversized request.
- Added a ProgressHandler to APIEndpointExportRunner.export so the manual
  UI can still show per-date progress across a multi-batch run.
- When a batch upload fails, report every date in that batch's records
  (not just the batch start date) plus every date in every not-yet-attempted
  subsequent batch, so failed-date consumers see the full picture instead of
  a single date.
- Carry a batch's failed-date details forward to the next batch that does
  upload successfully (instead of dropping them), so the endpoint still
  learns about failure-only batches. If no later batch succeeds, flush them
  in a dedicated failure-only request -- but only if at least one batch did
  upload; an entirely-empty range still sends nothing, matching prior
  behavior.
- Only add a batch's external/provider record count to the result after its
  upload actually succeeds, not when the records are fetched, so a rejected
  batch's provider records aren't counted as delivered.

New tests: testFailureOnlyBatchIsCarriedForwardToNextSuccessfulUpload,
testTrailingFailureOnlyBatchIsFlushedInDedicatedUpload,
testExternalRecordCountOnlyIncludesSuccessfullyUploadedBatches, and expanded
assertions in testBatchFailureStopsSubsequentBatchesAndPreservesPriorSuccess
to check every skipped date is reported.
ContentView.exportDataToAPIEndpoint previously duplicated the per-date
fetch loop and called APIExportClient.upload directly with the whole
range in one request -- the batching added to APIEndpointExportRunner
never applied to this path, which is the one users actually hit when
exporting a large history range from the app. It now calls
APIEndpointExportRunner.export (with an onProgress callback preserving
the existing per-date progress UI) and maps the returned ExportResult
to status messages the same way exportLocalData already does for the
vault target.

apiExportFailureReason is still used by the Mac export streaming path
(streamConnectedMacExport), so it's kept in place.
@Just-Insane

Copy link
Copy Markdown
Contributor Author

Pushed fixes for all four review comments (replies inline on each thread):

  1. Manual export now routed through the batch runner (P1). ContentView.exportDataToAPIEndpoint previously duplicated the fetch loop and called APIExportClient.upload once directly, so batching never applied to the actual user-facing large-history-export flow. It now calls APIEndpointExportRunner.export, with a new onProgress callback so the UI keeps its per-date progress bar across multiple batches.
  2. Every skipped date reported on batch failure (P1). A failed batch now reports every date in its own records plus every date in every not-yet-attempted subsequent batch (marked .unknown/"not attempted", distinct from the actual .fileWriteError), instead of just the batch start date.
  3. Failure-only batches no longer silently dropped (P2). A batch with no deliverable records carries its failed_date_details forward to the next batch that does upload successfully; if none follows, they're flushed via a dedicated failure-only request (only when at least one batch already succeeded, preserving the "entirely empty range sends nothing" behavior).
  4. External record count only reflects delivered batches (P2). externalRecordFileCount is now incremented only after a batch's upload succeeds, not when the records are fetched.

Added tests: testFailureOnlyBatchIsCarriedForwardToNextSuccessfulUpload, testTrailingFailureOnlyBatchIsFlushedInDedicatedUpload, testExternalRecordCountOnlyIncludesSuccessfullyUploadedBatches, plus expanded assertions in testBatchFailureStopsSubsequentBatchesAndPreservesPriorSuccess.

Note: I pushed one bad intermediate commit to ContentView.swift (accidentally sent placeholder content instead of the real file) and immediately corrected it in the following commit — the branch head is correct and verified byte-for-byte against the intended content. Still unable to run xcodebuild test in this environment, so as before, please run the test target before merging.

@CodyBontecou

Copy link
Copy Markdown
Owner

Thanks for submitting the issue and pr - let me look over it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d765f9b1d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 251 to 253
return ExportOrchestrator.ExportResult(
successCount: records.count,
successCount: totalSuccessCount,
totalCount: normalizedDates.count,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep partially failed scheduled exports pending

When a scheduled lookback exceeds seven days and a later batch upload fails, this returns a nonzero successCount from earlier batches. The checked scheduled path passes this result to ScheduledExportCoordinator.completePendingScheduledExport, whose successCount > 0 branch clears the entire pending request, while SchedulingManager also advances the last-export date; consequently, the failed and unattempted dates are not automatically retried even though they now appear in failedDateDetails.

Useful? React with 👍 / 👎.

Comment on lines +155 to +159
if Task.isCancelled {
return ExportOrchestrator.ExportResult(
successCount: totalSuccessCount + batchRecords.count,
totalCount: normalizedDates.count,
failedDateDetails: allFailedDateDetails + pendingUnsentFailedDateDetails + batchFailedDateDetails,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude unuploaded batch records when cancellation occurs

If the user cancels after one or more records have been fetched in the current batch but before that batch reaches the upload call, batchRecords.count is added to the success count even though those records never left the device. ContentView persists this result, consumes export quota, and reports those days as “uploaded,” while they are absent from the failed-date list used for retries; cancellation should count only previously completed uploads.

Useful? React with 👍 / 👎.

Comment on lines +214 to +221
_ = try await upload(
batchRecords,
combinedFailedDateDetailsForUpload,
batchExternalRecords,
settings,
apiSettings,
batchStart,
batchEnd

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep carried failures inside the uploaded date range

When an all-empty batch precedes a batch containing records, combinedFailedDateDetailsForUpload includes dates from that earlier batch, but the envelope range is still passed as the current batchStart...batchEnd. The endpoint therefore receives failed_date_details outside the request's declared date_range, which can cause range-validating consumers to reject or disregard those failures; the range must also cover the carried dates.

Useful? React with 👍 / 👎.

Comment on lines +283 to +284
allFailedDateDetails.append(contentsOf: pendingUnsentFailedDateDetails)
pendingUnsentFailedDateDetails = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid duplicating trailing failed-date details

For a successful batch followed by a trailing batch with no deliverable records, line 208 has already appended that batch's failures to allFailedDateDetails while also retaining them in pendingUnsentFailedDateDetails; appending the pending array again here records every trailing failure twice. Export history then displays duplicate dates, and its retry flow maps the duplicates directly into repeated export attempts.

Useful? React with 👍 / 👎.

Comment on lines +227 to +236
} catch {
allFailedDateDetails.append(contentsOf: batchFailedDateDetails)

// Every date whose record was part of this failed upload is
// itself undelivered — report each one, not just the batch
// start, so consumers (e.g. retry UI) can see exactly which
// dates need to be re-sent.
let failureMessage = "Batch upload failed for \(rangeDescription(start: batchStart, end: batchEnd)): \(error.localizedDescription)"
allFailedDateDetails.append(contentsOf: batchRecords.map {
FailedDateDetail(date: $0.date, reason: .fileWriteError, errorDetails: failureMessage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface the batch upload error as the primary failure

If a batch contains both a no-data date and records whose upload is rejected, the per-date .noHealthData details are appended before the .fileWriteError details created for the rejected POST. Because ExportResult.primaryFailureReason and the manual failure dialog use the first detail, a 401/413/500 failure is presented as “No Health Data,” hiding the actionable endpoint error from the user; place the upload failure first or otherwise select it as primary.

Useful? React with 👍 / 👎.

Comment on lines +268 to +271
let flushStart = pendingUnsentFailedDateDetails.map(\.date).min() ?? dateRangeStart
let flushEnd = pendingUnsentFailedDateDetails.map(\.date).max() ?? dateRangeEnd
do {
_ = try await upload([], pendingUnsentFailedDateDetails, [], settings, apiSettings, flushStart, flushEnd)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the API envelope's nonempty-record contract

When earlier batches succeeded but the trailing batch contains only failed dates, this sends a healthmd.api_export envelope with records: [] and record_count: 0. The public contract in docs/reference/index.md:30 defines this envelope as containing one or more daily records, and prior behavior deliberately skipped uploads when no records existed, so existing receivers may reject this new failure-only request; reporting failures without records requires an explicitly supported/versioned envelope contract or a different delivery mechanism.

Useful? React with 👍 / 👎.

@CodyBontecou

Copy link
Copy Markdown
Owner

I implemented the five review fixes and pushed them as a one-commit patch. Because maintainer edits are disabled on this fork, I could not write directly to this PR branch.

Please merge Just-Insane#1 into fix/batch-api-endpoint-exports; that will update this PR automatically.

Commit: ee6e31d9

Validation completed locally:

  • iOS: 1,280 passed, 4 skipped
  • macOS: 1,309 passed, 1 skipped
  • export-focused/contract tests: 57 passed
  • make check-export-docs: passed
  • git diff --check: passed

@CodyBontecou

Copy link
Copy Markdown
Owner

Follow-up: an independent audit found no remaining production-path defects. I added extra regression coverage for cancellation after an earlier successful batch, failure-detail uniqueness, serialized range scoping, generic transport-error redaction, silent-push partial batches, and Today Refresh markers.

Patch PR Just-Insane#1 now includes commit 25a3dfa9; the new focused run passes 32/32 tests.

@CodyBontecou

Copy link
Copy Markdown
Owner

Patch PR follow-up a25d9477 addresses the latest review note: scheduled completion now tracks durably reported dates separately from retained-record successes, so successful no-data metadata clears pending work without treating failed/cancelled uploads as complete.

Validation after the fix:

  • iOS: 1,288 passed, 4 skipped
  • macOS: 1,314 passed, 1 skipped
  • focused completion/batching suites: 58 passed

Requested a fresh Codex review on Just-Insane#1.

@Just-Insane

Copy link
Copy Markdown
Contributor Author

Merged your fixes into this PR's branch on my fork

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0d5c95517

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

) {
self.successCount = successCount
self.totalCount = totalCount
self.completedDateCount = completedDateCount ?? successCount

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count terminal no-data dates as completed

When a scheduled local-folder or Connected Mac export succeeds for some dates but reports .noHealthData for another, those existing producers omit completedDateCount, so this default sets it below totalCount. The new coordinator and scheduling checks then preserve the pending request and do not advance the schedule, causing the same successful dates and permanently empty date to be exported repeatedly. Either these producers must count terminal no-data outcomes as completed or the new retry decision must distinguish them from retryable failures.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 644a6ce1 (patch PR Just-Insane#2). ExportResult now carries exact terminal dates. iPhone-folder no-data outcomes complete, retryable HealthKit/device-lock failures remain pending, Connected Mac reports exact source-device dates, and legacy Mac peers are capability-gated. Residual requests contain only unresolved dates.

batchFailedDateDetails,
batchExternalRecords,
settings,
apiSettings,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Snapshot the API destination before uploading batches

During a manual multi-batch export, apiSettings is the live mutable @StateObject, and the endpoint settings sheet remains reachable while the async export yields between batches. If the user edits the endpoint or token after an earlier batch uploads, subsequent batches read the changed values here and can send portions of the same health export to a different server or account. Capture an immutable endpoint and authorization snapshot when the export starts and use it for every batch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 644a6ce1 (patch PR Just-Insane#2). Manual API export captures an immutable APIExportDestinationSnapshot before the task starts; every batch and the history label use the same endpoint, authorization value, and display name. Regression coverage mutates live settings after batch 1 and verifies batch 2 still uses the original snapshot.

Comment thread HealthMd/iOS/SchedulingManager.swift Outdated
Comment on lines 1624 to 1627
} else {
logger.info("Scheduled export partially completed; pending dates preserved for retry")
}
await sendExportNotification(success: true, daysExported: result.successCount)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Notify users when a scheduled export is only partial

When an earlier API batch succeeds but a later batch fails, this branch correctly recognizes that pending dates remain, but it immediately sends success: true. sendExportNotification renders that as “Export Completed” and “Successfully exported …,” so a background user receives no indication that part of the requested range failed and remains pending. Use a partial/failure notification whenever didCompleteAllRequestedDates is false.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 644a6ce1 (patch PR Just-Insane#2). Incomplete automatic runs no longer post “Export Completed.” The coordinator persists a reduced stable-ID pending request and sends the actionable “Health Export Needs Attention” notification, whose payload retries only unresolved dates.

@CodyBontecou

Copy link
Copy Markdown
Owner

Follow-up fixes for the latest review are ready in Just-Insane#2 (644a6ce1). Because maintainer edits remain disabled, merging that patch PR will update this PR automatically.

Highlights:

  • exact per-date residual retries (no lost locked dates or duplicated completed local days)
  • immutable API endpoint/auth snapshot across batches
  • truthful actionable partial notifications
  • cross-timezone/noncontiguous Connected Mac completion tracking with additive capability negotiation
  • archive/summary durability and append-retry safeguards

Validation on the final tree:

  • iOS: 1,294 passed, 4 skipped
  • macOS: 1,322 passed, 1 skipped
  • generated docs current; 219 links valid
  • git diff --check passed

No daily export-schema change; sync additions are optional/capability-negotiated.

@CodyBontecou

Copy link
Copy Markdown
Owner

Added 5a7c4bb0 to follow-up patch Just-Insane#2: removes the 366-day user cap so CLI, Mac→iPhone, and Shortcuts exports accept multi-year corpus ranges. Transfer-byte and inactivity safeguards remain, and malformed exact-date metadata is rejected without adding another day-count ceiling. Final validation: iOS 1,294 passed/4 skipped; macOS 1,325 passed/1 skipped; CLI 13 passed.

@CodyBontecou

Copy link
Copy Markdown
Owner

Final follow-up is ready in Just-Insane#2 at 32be5186. It now includes the exact retry fixes, removal of the 366-day cap, and capability-negotiated resumable corpus exports above 2 GiB with bounded partitions/items, durable cancellation/reconnect recovery, streamed ZIP64/rollups, and streamed strict raw CLI output. Validation: iOS 1,321 passed (5 skipped), macOS 1,364 passed (2 skipped), CLI 16 passed, physical >2 GiB aggregate test passed, both Xcode analyzers succeeded, docs/current links passed, and final independent correctness/security reviews found no remaining P0/P1 blockers. Daily export schema/version is unchanged; legacy peers retain the 2 GiB path.

Fix exact retries and add resumable corpus exports

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

let earliest = calendar.date(byAdding: .day, value: -(requestedDays - 1), to: yesterday)!

P2 Badge Reject oversized Shortcut lookbacks before expanding

When a Shortcut supplies an extremely large days value now that the upper bound was removed, this date calculation can return nil (or otherwise leads to materializing an enormous [Date] on the main actor), and the force unwrap crashes the intent before it can report a validation error. Keep a practical cap or gracefully reject calendar overflow before expanding the requested range.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

_ = iphoneExportRequestCoordinator.complete(with: fileResult)
syncService.send(.macExportResult(fileResult))
}
syncService.send(.connectedCorpusTransferFinalAck(acknowledgement))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Complete strict-raw replay responses before ACKing

If the Mac finishes a partitioned strict-raw export, persists the terminal ACK, and the iPhone disconnects before receiving that ACK, a reconnect reaches this replay branch with fileResult == nil because strict-raw finalization never stores a terminalResult. The code then sends only connectedCorpusTransferFinalAck, leaving the active CLI/control request unresolved until its inactivity timeout instead of returning the already-finalized raw response; replay needs to complete or fail the pending raw control request as well.

Useful? React with 👍 / 👎.

Comment on lines +388 to +389
case .macHealthDay:
try await applyHealthDay(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject corpus item kinds that do not match the mode

For a strict-raw corpus request, requestExport skips Mac destination preflight because raw mode should not write files, but this switch applies any completed segment by its declared kind. If a malformed partition in a .strictRaw session carries a .macHealthDay item, this branch calls applyHealthDay and can write daily files to the vault; validate segment.kind against session.journal.exportManifest.mode before applying.

Useful? React with 👍 / 👎.

Comment on lines +874 to +878
session.journal.failedDateDetails.append(FailedDateDetail(
date: payload.sourceDate,
reason: .fileWriteError,
errorDetails: error.localizedDescription
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat corpus no-data records as terminal

Fresh evidence is the new corpus receiver path: the iPhone sends a successfully fetched but empty HealthData as record with failure == nil, and VaultManager.exportHealthData throws ExportError.noHealthData; this catch maps that to .fileWriteError and never adds the date to completedDates. In scheduled partitioned Connected Mac exports, permanently empty days therefore remain pending and are retried as write failures instead of being cleared as terminal no-data outcomes; handle ExportError.noHealthData separately here.

Useful? React with 👍 / 👎.

Comment on lines +751 to +753
session.journal.state = .cancelled
session.journal.updatedAt = Date()
try? persist(session)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not cancel completed corpus sessions

If the Mac has already finalized and persisted a corpus session but the iPhone misses the final ACK and later sends connectedCorpusTransferCancel, this path restores the completed journal and unconditionally changes it to .cancelled. That can overwrite a successful file export with a cancelled result (or destroy the replayable strict-raw terminal ACK) even though the files/raw response were already finalized; completed sessions should replay or reject cancellation instead of being downgraded.

Useful? React with 👍 / 👎.

@CodyBontecou
CodyBontecou merged commit 9da4b51 into CodyBontecou:main Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API Endpoint export fails/times out on large historical ranges

2 participants