mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
feat(orca-cloud): integrate Orca Cloud profile sync across UI, slicer and SpoolBuddy
Reads, lists, and slices with profiles from a user's Orca Cloud account (OrcaSlicer 2.4.0-alpha's Supabase-backed sync) alongside the existing Bambu Cloud integration. Four sign-in providers (Google / Apple / GitHub / email+password); password defaults. Paste-flow PKCE because Orca's Supabase project only allowlists localhost redirect_to — open feature request at OrcaSlicer/OrcaSlicer#14028. Surfaces: - Profiles tab: new "Orca Cloud" tab next to "Bambu Cloud" with the same rich layout (search + 5 filter dropdowns + 3-column grouped grid + read-only detail modal) - SliceModal: 4-tier preset picker (orca_cloud > local > bambu cloud > standard); separate status banner per cloud; metadata-aware pre-pick scores Orca filaments above local (Orca's sync_pull returns full content inline so filament_type / filament_colour come for free, no per-setting fetch rate-limit dance) - ConfigureAmsSlotModal: orca_cloud as a new preset source (prefixed orca_<UUID> to match local_/builtin_); generic Bambu filament-ID derivation from parsed material (printer firmware can't grok Orca UUIDs); slot mapping persists preset_source='orca_cloud' - SpoolForm / SpoolBuddyWriteTagPage: Orca filaments merge into the cloud preset list via Promise.allSettled (OrcaProfileMeta is structurally identical to SlicerSetting) Backend: - services/orca_cloud.py: OrcaCloudService with PKCE / token exchange / single-use refresh rotation / get_user_info / list_profiles via the bare /sync/pull bootstrap path - routes/orca_cloud.py: 7 endpoints (auth/start, auth/finish, auth/password, status, logout, profiles, profiles/{id}); router-level _cloud_api_key_gate + per-route cloud_caller() so API-keyed callers (SpoolBuddy kiosk) properly resolve their owner User; just-in-time refresh with atomic persist-before-API-call - routes/slicer_presets.py: _fetch_orca_cloud_presets mirrors the Bambu Cloud fetcher (status vocabulary, 5min cache, permission shortcut); _dedupe_by_name extended to 4 tiers; UnifiedPresetsResponse gains orca_cloud + orca_cloud_status - services/preset_resolver.py: PresetRef.source extended with "orca_cloud"; _resolve_orca_cloud walks list + filters - 8 columns on users table for tokens (5 persistent) + transient PKCE handshake state with 10-min TTL (3); dialect-branched DATETIME / TIMESTAMP; auth-disabled mode falls back to Settings table - orca_cloud:auth permission folded into can_access_cloud API-key scope (same trust dimension)
This commit is contained in:
@@ -122,23 +122,51 @@ export function SpoolFormModal({
|
||||
setRecentColors(loadRecentColors());
|
||||
}, []);
|
||||
|
||||
// Fetch cloud presets and catalog when modal opens
|
||||
// Fetch cloud presets and catalog when modal opens. Fetches Bambu Cloud
|
||||
// and Orca Cloud in parallel; merges Orca filaments into ``cloudPresets``
|
||||
// since ``OrcaProfileMeta`` is structurally identical to ``SlicerSetting``
|
||||
// (same fields, same semantics). ``cloudAuthenticated`` flips on if either
|
||||
// cloud is connected — the UI only uses it to gate "no cloud" hints.
|
||||
useEffect(() => {
|
||||
// ``cancelled`` gates every state setter so a fetch that resolves AFTER
|
||||
// the modal closes / unmounts can't fire setState on a torn-down
|
||||
// component. Without this guard the parallel Promise.allSettled chain
|
||||
// can still hit ``setLoadingCloudPresets(false)`` in its ``finally``
|
||||
// after vitest has dismantled the JSDOM window — surfaced as an
|
||||
// "Unhandled Rejection: window is not defined" in CI runs.
|
||||
let cancelled = false;
|
||||
if (isOpen) {
|
||||
const fetchData = async () => {
|
||||
setLoadingCloudPresets(true);
|
||||
try {
|
||||
const status = await api.getCloudStatus();
|
||||
setCloudAuthenticated(status.is_authenticated);
|
||||
if (status.is_authenticated) {
|
||||
const presets = await api.getFilamentPresets();
|
||||
setCloudPresets(presets);
|
||||
}
|
||||
const [bambuResult, orcaResult] = await Promise.allSettled([
|
||||
(async () => {
|
||||
const status = await api.getCloudStatus();
|
||||
if (!status.is_authenticated) return { connected: false, presets: [] as SlicerSetting[] };
|
||||
const presets = await api.getFilamentPresets();
|
||||
return { connected: true, presets };
|
||||
})(),
|
||||
(async () => {
|
||||
const status = await api.orcaCloudStatus();
|
||||
if (!status.connected) return { connected: false, presets: [] as SlicerSetting[] };
|
||||
const list = await api.orcaCloudListProfiles();
|
||||
// OrcaProfileMeta is structurally identical to SlicerSetting.
|
||||
return { connected: true, presets: list.filament as unknown as SlicerSetting[] };
|
||||
})(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
const bambuConnected = bambuResult.status === 'fulfilled' && bambuResult.value.connected;
|
||||
const orcaConnected = orcaResult.status === 'fulfilled' && orcaResult.value.connected;
|
||||
const bambuPresets = bambuResult.status === 'fulfilled' ? bambuResult.value.presets : [];
|
||||
const orcaPresets = orcaResult.status === 'fulfilled' ? orcaResult.value.presets : [];
|
||||
setCloudAuthenticated(bambuConnected || orcaConnected);
|
||||
setCloudPresets([...bambuPresets, ...orcaPresets]);
|
||||
} catch (e) {
|
||||
if (cancelled) return;
|
||||
console.error('Failed to fetch cloud presets:', e);
|
||||
setCloudAuthenticated(false);
|
||||
} finally {
|
||||
setLoadingCloudPresets(false);
|
||||
if (!cancelled) setLoadingCloudPresets(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
@@ -195,6 +223,9 @@ export function SpoolFormModal({
|
||||
// "test environment was torn down" errors in vitest. spoolmanMode only
|
||||
// gates a single fetch (getSpoolCatalog) which is cheap enough to skip
|
||||
// when the modal opens in Spoolman mode.
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, printersWithCalibrations.length]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user