Skip to content

Integrate CollabCraft cloud Open/Save/Share/presence - #2765

Open
wdunn001 wants to merge 35 commits into
storytold:mainfrom
wdunn001:cloud-pages
Open

wdunn001 wants to merge 35 commits into
storytold:mainfrom
wdunn001:cloud-pages

Conversation

@wdunn001

@wdunn001 wdunn001 commented Oct 16, 2026 •

Copy link
Copy Markdown

Summary

Integrate CollabCraft cloud Open/Save/Share/presence for PDFCraft.

Note: Diff is huge mostly from i18n .tsv line churn, not new feature surface. Needs rebase — conflicting; consider dropping accidental i18n rewrites before review.


Related to storytold/craft-libs#123 (Add collab-craft repo and accept transfer).

wdunn001 and others added 20 commits October 15, 2026 01:49
Adds provider-neutral craft-ui cloud integration, following the same
pattern as wordcraft, deckcraft, and designcraft.

craft-ui upgraded from 5c874d51 to 289d562 (rev that carries the cloud
feature). craft-cloud, craft-cloud-client, and collabcraft-share added
at the matching revs.

crates/ui-egui:
- CloudApp implementation for PdfCraftApp: PDF-only writes_as, full
  encode via save_full_bytes, open via open_bytes, close by tab index.
  Presence is viewer-only: no real-time co-editing is claimed.
- paint_on helper in icons.rs for painting SVG icons from a Painter
  without a Ui (required by CloudApp::paint_icon contract).
- cloud.rs and cloud_tests.rs: state machine tests with an in-memory
  service covering account, conflict, writes_as enforcement, and kind.
- lib.rs: CloudFiles, ShareDialog, PresenceBar fields; cloud::pump call
  added to logic().
- chrome.rs: cloud controls (who badge, share button) placed in the
  right-to-left section of tab_strip, left of theme/help, no overlap.

apps/pdfcraft (desktop):
- cloud.rs: blocking worker thread, device-flow sign-in with 300s
  timeout, conditional writes with etag, Fault::Conflict on 409/412,
  token file beside app settings (0o600 on Unix), platform-specific
  browser open (cmd on Windows, open on macOS, xdg-open on Linux).
- main.rs: wires cloud::service and collabcraft_share into app.cloud
  and app.share; absent PDFCRAFT_CLOUD_URL leaves the app local-only.

apps/pdfcraft-web (browser):
- cloud.rs: async wasm_bindgen_futures transport, same-origin cookie
  session, no device flow, logout via POST to /auth/logout.
- main.rs: wires Service::new(ctx) and CollabCraft::from_env into the
  app; graceful when not served from a CollabCraft origin.
Two security hardening fixes to the CollabCraft desktop transport.

write_private (Unix): replace std::fs::write + set_permissions with
OpenOptions::mode(0o600) so the file is created owner-only from the
instant of its first inode, closing the write-then-chmod race window.
A second set_permissions call tightens any file an older version wrote
under a loose umask. Windows path unchanged (AppData is owner-scoped).

token_for_service: load_token no longer ignores the stored service URL.
The extracted token_for_service helper compares the saved service origin
to the currently configured one (normalising trailing slashes). A
mismatch logs a warning and returns None, preventing a saved session
from being forwarded to a different deployment after the URL env-var
changes.

Tests added in apps/pdfcraft/src/cloud.rs:
- token_accepted_when_service_matches_exactly
- token_accepted_when_trailing_slash_differs
- token_rejected_when_service_url_changes
- token_rejected_when_service_field_absent
- empty_token_is_treated_as_absent
- write_private_creates_file_with_mode_0600 (Unix)
- write_private_tightens_existing_file_with_loose_permissions (Unix)
egui::Ui::new dropped the layer_id parameter in 0.36; remove the
extra argument from paint_on in icons.rs.

theme::bold does not exist; use FontFamily::Name(semibold) which
matches pdfcraft's heading style for cloud column headings.
cloud::start() was only called from open_page() (the File > Open dialog).
In the browser build the session rides the same-origin cookie set at
craft.quasarke.net login. Without start() in the update loop the app
never submitted Job::Account, so app.cloud.account stayed None, the
title-bar account badge showed Sign in, and all capability-gated
controls (Share, Talk, provider-linked checks) never appeared.

Calling start() from update() is idempotent: it guards with asked_account
and acts only once. open_page() still calls it too; the second call is
a no-op. The new test verifies the account arrives without an explicit
start() call, covering the browser cookie path.
After the bootstrap fix (start() in update()), two routing bugs appeared:

Sign-out loop: Job::SignOut called window.location().reload(), which
reloaded the PDFCraft page, triggering start() -> Job::Account ->
Unauthenticated -> offer_login() -> /auth/login immediately. Fixed by
navigating to / instead of reloading; the root page has no automatic
credential check.

Callback landing: the CollabCraft OIDC callback always redirects to /
(server limitation, pinned rev 80bfaec). Opening login in a popup
window keeps the user on the PDFCraft page. A setInterval closure
watches for the popup to close, then stores 'pdfcraft:auth_done' in
egui's ephemeral memory and requests a repaint. The update loop
consumes the flag, marks a 'popup_retry' (so a cancelled popup does
not immediately reopen), and calls sign_in() to force a fresh
Job::Account. When the popup is blocked by the browser the transport
falls back to the original full-page redirect to /auth/login.

Also fix a compile error in the test suite: open_page_lists_root_when_linked
referenced cloud.folders which does not exist on CloudFiles; corrected
to cloud.mine.loaded. Add sign_out_does_not_leave_unauthenticated_fault
which asserts that after sign-out the re-check returns NotLinked (link
prompt) rather than Unauthenticated (login redirect), documenting the
protocol that prevents the sign-out loop.

Co-authored-by: Cursor <cursoragent@cursor.com>
cloud_mut() is a CloudApp trait method; CloudApp is not in scope in
update(), which lives in the root lib.rs module. Accessing self.cloud.status
directly avoids the missing trait import and is equivalent since PdfCraftApp
implements CloudApp with &mut self.cloud.

Co-authored-by: Cursor <cursoragent@cursor.com>
web-sys Window::closed() is fallible in this version.
Use unwrap_or(true) so a JS error treats the popup as closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…trip entry points

- Add CloudPage enum (Open, SaveAs) and cloud_page field to PdfCraftApp
- Render craft_ui::cloud::open_page in CentralPanel when cloud_page = Open
- Render craft_ui::cloud::save_page in CentralPanel when cloud_page = SaveAs
- cloud_page_shell wraps pages with scrollable frame, heading, and Back button
- Auto-dismiss Open page once a new document tab appears after cloud open
- Implement go_to_save_as() so conflict modal can redirect to cloud Save As
- file.save routes to cloud::save() when document has a cloud origin (etag write)
- file.open_cloud and file.save_as_cloud commands added to File menu
- Commands absent (not just dimmed) when no cloud service is connected
- Tab strip Open button becomes popup (local/cloud) when cloud is available
- Accessibility/regression tests: visible headings, Back button, command availability,
  cloud open/save exercised from UI state path, etag write verified, go_to_save_as tested
Co-authored-by: Cursor <cursoragent@cursor.com>
Unblocks ui-egui catalog tests for cloud menu labels and command ids.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@wdunn001

Copy link
Copy Markdown
Author

Request signatures (CollabCraft e-sign UI) is now on this branch: merge of esign-ui at 8566d2a4 (host transport + chrome button + craft-esign / collab-craft 23a3b66 pins).

This branch has not been deployed

No deployments
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.

1 participant