Commit Graph
6 Commits
Author SHA1 Message Date
kua-agentandClaude Fable 5.1 bd11ba96c5 fix: MAJOR — send() race between concurrent transport switch and an in-flight send
Two issues: (1) directoryExists(at:) only checked existence + isDirectory, so
an existing-but-unwritable OneDrive folder skipped the
oneDriveFolderUnavailable branch and surfaced as a generic pdfCompositionFailed
message instead — now uses OneDriveLocator.isWritableDirectory(at:). (2)
send() read `self.outboxURL` again inside composePDFForSend() after at least
one await had already run, so a concurrent chooseTransport() call
(SettingsView.swift) could flip transport/outboxURL/recordUncommented
mid-send, landing the PDF under one transport's folder while the
archive/status branch ran the other's.

Fix: send() now snapshots BOTH transport and the destination folder into
local `let`s once, before any await, and passes the folder explicitly into
the renamed composePDFForSend(outbox:) — which no longer reads
self.outboxURL at all. The archive/status switch already used the frozen
`transport` local. (chooseTransport/chooseOneDriveFolder additionally refuse
outright while isSending is true — see the SettingsView.swift commit — so in
practice this race can no longer even be triggered, but the snapshot is the
actual structural fix regardless.)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:27:49 +04:00
kua-agentandClaude Fable 5.1 209921f084 feature: model-owned resolvedOneDriveFolder + OneDrive Settings/menu panel snapshots
SettingsView's OneDrive row called OneDriveLocator.resolveOneDriveFolder()
directly with the real UserDefaults.standard and the real home directory,
which made that row impossible to drive from a fake/isolated environment.
Moved that resolution into AppModel as a tracked resolvedOneDriveFolder
property (nil means "no OneDrive folder found"), refreshed at init,
bootstrap, chooseTransport, chooseOneDriveFolder, and inside send()'s live
folder check. SettingsView and chooseOneDriveFolder's picker-start path now
read model.resolvedOneDriveFolder instead of calling OneDriveLocator
directly — state flows through the model like everything else in this app.

PanelSnapshot (SHOTDECK_SNAPSHOT_DIR) adds three panels on a SEPARATE
isolated model so the transport switch never bleeds into the six existing
AirDrop-mode panels:
- panel-07-settings-onedrive.png: transport=oneDrive with a resolved folder,
  built by pointing OneDriveLocator.defaultRedlineFolder at a fake home tree
  (Library/CloudStorage/OneDrive-MMDGROUP under this snapshot's own temp
  root) so the displayed path is shaped like the real default without ever
  touching the real home.
- panel-08-settings-onedrive-missing.png: a fake home with no
  Library/CloudStorage at all, resolved through a throwaway UserDefaults
  suite (never .standard) so the "no OneDrive folder found" state and its
  still-usable Choose... button are exercised for real.
- panel-09-captures-present-onedrive.png: 3 captures + transport=oneDrive,
  confirming the menu row reads "Send to OneDrive".

Extracted the 3-swatch capture seeding (panel 04) into addSampleCaptures(to:)
so panel 09 reuses it instead of duplicating the loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:10:59 +04:00
kua-agentandClaude Fable 5.1 6449c72b3b feature: OneDrive folder as a second send transport
send(anchor:) now branches on TransportSettings.transport(). OneDrive mode
skips AirDrop entirely: it verifies the resolved OneDrive folder exists right
before composing (never trusts stale state), archives the session immediately
after the PDF lands, and sets "Saved to OneDrive — N page(s). Open it in Files
on your iPad." AirDrop's existing behaviour, including handleDidFailToShareItems,
is untouched and only reached from the .airDrop branch.

AppModel seeds outbox/watch from TransportSettings.effectiveFolders() instead
of FolderSettings.resolve() directly, tracks the live `transport`, and
bootstrap() creates the OneDrive folder and sets the watcher's
recordUncommented flag (true only for AirDrop) before the watcher starts.

Settings gets a "Send via" segmented picker above Folders. AirDrop shows the
existing watch/output rows; OneDrive shows a single read-only OneDrive folder
row (Choose... reuses the existing directory picker) plus one caption
explaining the same-folder round trip, or a "No OneDrive folder found" prompt
when nothing resolves (Choose... stays usable). Switching transport re-points
the watcher's folder and recordUncommended live; AirDrop's own folder
overrides are stored separately and are untouched by a OneDrive-and-back
round trip.

Menu's "Send..." row reads "Send to OneDrive" when that transport is active.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
2026-09-05 09:01:13 +04:00
kua-agent 5e61cd735c fix: archive only after AirDrop completes; PDF named by send time; Reveal last PDF 2026-09-02 09:12:08 +04:00
kua-agent d05bd735b5 rename: user-facing product name Shotdeck -> Redline; legacy PDFs still recognized 2026-09-01 22:25:15 +04:00
kua-agent 9201e74bfc WP-4b: Send pipeline + AirDrop sheet per SPEC-A2b 2026-08-31 16:12:58 +04:00