Skip to content

Commit bcb87d8

Browse files
committed
fix(api): add safety checks for Odoo supplier sync v2.1.11
1 parent c4ad651 commit bcb87d8

24 files changed

+6005
-164
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to BluePLM will be documented in this file.
44

5+
## [2.2.0] - UNRELEASED
6+
7+
### Added
8+
- **Webhooks Integration**: Send HTTP notifications to external services when events occur in BluePLM
9+
- Configure webhooks in Settings → Webhooks (admin-only)
10+
- 12 event types: file created/updated/deleted, check-in/check-out, state changes, reviews, ECOs
11+
- HMAC-SHA256 signature verification for secure payloads
12+
- User filtering: trigger webhooks for everyone, specific roles, or specific users
13+
- Built-in test button to verify webhook endpoints
14+
- Delivery history with status tracking (success, failed, retrying)
15+
- Auto-retry failed deliveries with configurable retry count and delay
16+
- Enable/disable toggle per webhook
17+
18+
---
19+
520
## [2.1.1] - 2025-12-12
621

722
### Fixed

api/server.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -610,39 +610,57 @@ async function fetchOdooSuppliers(
610610
): Promise<{ success: boolean; suppliers: OdooSupplier[]; error?: string }> {
611611
const normalizedUrl = normalizeOdooUrl(url)
612612
try {
613+
console.log('[Odoo] Authenticating to:', normalizedUrl)
614+
613615
// Authenticate first
614616
const uid = await odooXmlRpc(normalizedUrl, 'common', 'authenticate', [
615617
database, username, apiKey, {}
616618
])
617619

620+
console.log('[Odoo] Auth result uid:', uid)
621+
618622
if (!uid || uid === false) {
619623
return { success: false, suppliers: [], error: 'Authentication failed' }
620624
}
621625

622626
// Search for suppliers (partners with supplier_rank > 0)
627+
console.log('[Odoo] Searching for suppliers...')
623628
const supplierIds = await odooXmlRpc(normalizedUrl, 'object', 'execute_kw', [
624629
database, uid, apiKey,
625630
'res.partner', 'search',
626631
[[['supplier_rank', '>', 0]]],
627632
{ limit: 5000 } // Reasonable limit
628-
]) as number[]
633+
])
629634

630-
if (!supplierIds || supplierIds.length === 0) {
635+
console.log('[Odoo] Supplier IDs result:', typeof supplierIds, Array.isArray(supplierIds) ? supplierIds.length : supplierIds)
636+
637+
// Ensure supplierIds is an array
638+
const ids = Array.isArray(supplierIds) ? supplierIds : []
639+
640+
if (ids.length === 0) {
641+
console.log('[Odoo] No suppliers found')
631642
return { success: true, suppliers: [] }
632643
}
633644

634645
// Read supplier details
635-
const suppliers = await odooXmlRpc(normalizedUrl, 'object', 'execute_kw', [
646+
console.log('[Odoo] Reading', ids.length, 'supplier details...')
647+
const suppliersResult = await odooXmlRpc(normalizedUrl, 'object', 'execute_kw', [
636648
database, uid, apiKey,
637649
'res.partner', 'read',
638-
[supplierIds, [
650+
[ids, [
639651
'id', 'name', 'ref', 'email', 'phone', 'mobile', 'website',
640652
'street', 'street2', 'city', 'zip', 'state_id', 'country_id', 'active'
641653
]]
642-
]) as OdooSupplier[]
654+
])
655+
656+
console.log('[Odoo] Suppliers result type:', typeof suppliersResult, Array.isArray(suppliersResult) ? suppliersResult.length : 'not array')
657+
658+
// Ensure result is an array
659+
const suppliers = Array.isArray(suppliersResult) ? suppliersResult as OdooSupplier[] : []
643660

644661
return { success: true, suppliers }
645662
} catch (err) {
663+
console.error('[Odoo] Error:', err)
646664
return { success: false, suppliers: [], error: String(err) }
647665
}
648666
}
@@ -3483,10 +3501,25 @@ export async function buildServer(): Promise<FastifyInstance> {
34833501
return reply.code(400).send({ error: 'Sync failed', message: odooSuppliers.error })
34843502
}
34853503

3504+
// Safety check - ensure suppliers is an array
3505+
const suppliers = Array.isArray(odooSuppliers.suppliers) ? odooSuppliers.suppliers : []
3506+
console.log(`[Odoo Sync] Found ${suppliers.length} suppliers from Odoo`)
3507+
3508+
if (suppliers.length === 0) {
3509+
return {
3510+
success: true,
3511+
created: 0,
3512+
updated: 0,
3513+
skipped: 0,
3514+
errors: 0,
3515+
message: 'No suppliers found in Odoo with supplier_rank > 0'
3516+
}
3517+
}
3518+
34863519
// Process suppliers
34873520
let created = 0, updated = 0, skipped = 0, errors = 0
34883521

3489-
for (const odooSupplier of odooSuppliers.suppliers) {
3522+
for (const odooSupplier of suppliers) {
34903523
try {
34913524
// Check if supplier already exists by erp_id
34923525
const { data: existing } = await request.supabase!

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-plm",
3-
"version": "2.1.10",
3+
"version": "2.1.11",
44
"description": "Product Lifecycle Management for engineering teams",
55
"main": "dist-electron/main.js",
66
"scripts": {

src/App.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react'
22
import { usePDMStore } from './stores/pdmStore'
33
import { SettingsContent } from './components/SettingsContent'
44

5-
type SettingsTab = 'account' | 'vault' | 'organization' | 'branding' | 'metadata-columns' | 'backup' | 'solidworks' | 'google-drive' | 'odoo' | 'slack' | 'webhooks' | 'api' | 'preferences' | 'logs' | 'about'
5+
type SettingsTab = 'profile' | 'preferences' | 'vault' | 'organization' | 'branding' | 'metadata-columns' | 'backup' | 'solidworks' | 'google-drive' | 'odoo' | 'slack' | 'webhooks' | 'api' | 'logs' | 'about'
66
import { supabase, getCurrentSession, isSupabaseConfigured, getFilesLightweight, getCheckedOutUsers, linkUserToOrganization, getUserProfile, setCurrentAccessToken, registerDeviceSession, startSessionHeartbeat, stopSessionHeartbeat } from './lib/supabase'
77
import { subscribeToFiles, subscribeToActivity, unsubscribeAll } from './lib/realtime'
88
// Backup services removed - now handled directly via restic
@@ -71,16 +71,16 @@ function useTheme() {
7171
const seasonalTheme = getSeasonalThemeOverride()
7272

7373
// If we're in a seasonal period and user's theme is NOT already the seasonal theme,
74-
// auto-switch to the seasonal theme
74+
// auto-switch to the seasonal theme (only once per season)
7575
if (seasonalTheme && theme !== seasonalTheme) {
76-
// Check if we've already auto-switched this month (stored in sessionStorage)
76+
// Check if we've already auto-switched this season (stored in localStorage to persist across restarts)
7777
const storageKey = `seasonal-theme-applied-${seasonalTheme}`
78-
const alreadyApplied = sessionStorage.getItem(storageKey)
78+
const alreadyApplied = localStorage.getItem(storageKey)
7979

8080
if (!alreadyApplied) {
8181
// Auto-switch to seasonal theme
8282
setTheme(seasonalTheme)
83-
sessionStorage.setItem(storageKey, 'true')
83+
localStorage.setItem(storageKey, 'true')
8484
console.log(`🎃🎄 Auto-applying ${seasonalTheme} theme for the season!`)
8585
}
8686
}
@@ -175,7 +175,16 @@ function App() {
175175
const [isResizingSidebar, setIsResizingSidebar] = useState(false)
176176
const [isResizingDetails, setIsResizingDetails] = useState(false)
177177
const [isResizingRightPanel, setIsResizingRightPanel] = useState(false)
178-
const [settingsTab, setSettingsTab] = useState<SettingsTab>('account')
178+
const [settingsTab, setSettingsTab] = useState<SettingsTab>('profile')
179+
180+
// Listen for settings tab navigation from MenuBar buttons
181+
useEffect(() => {
182+
const handleNavigateSettingsTab = (e: CustomEvent<SettingsTab>) => {
183+
setSettingsTab(e.detail)
184+
}
185+
window.addEventListener('navigate-settings-tab', handleNavigateSettingsTab as EventListener)
186+
return () => window.removeEventListener('navigate-settings-tab', handleNavigateSettingsTab as EventListener)
187+
}, [])
179188

180189
// Track if Supabase is configured (can change at runtime)
181190
const [supabaseReady, setSupabaseReady] = useState(() => isSupabaseConfigured())

src/components/MenuBar.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useRef, useCallback } from 'react'
2-
import { LogOut, ChevronDown, Building2, Search, File, Folder, LayoutGrid, Database, ZoomIn, Minus, Plus, RotateCcw, Monitor, Laptop, Loader2, Settings } from 'lucide-react'
2+
import { LogOut, ChevronDown, Building2, Search, File, Folder, LayoutGrid, Database, ZoomIn, Minus, Plus, RotateCcw, Monitor, Laptop, Loader2, User, SlidersHorizontal } from 'lucide-react'
33
import { usePDMStore } from '../stores/pdmStore'
44
import { signInWithGoogle, signOut, isSupabaseConfigured, linkUserToOrganization, getActiveSessions, endRemoteSession, UserSession, supabase } from '../lib/supabase'
55
import { getInitials } from '../types/pdm'
@@ -643,17 +643,33 @@ export function MenuBar({ minimal = false }: MenuBarProps) {
643643
</div>
644644
</div>
645645

646-
{/* Profile & Settings */}
646+
{/* Profile & Preferences */}
647647
<div className="py-1 border-b border-plm-border">
648648
<button
649649
onClick={() => {
650650
setShowUserMenu(false)
651651
setActiveView('settings')
652+
setTimeout(() => {
653+
window.dispatchEvent(new CustomEvent('navigate-settings-tab', { detail: 'profile' }))
654+
}, 0)
652655
}}
653656
className="w-full flex items-center gap-3 px-4 py-2 text-sm text-plm-fg hover:bg-plm-bg-lighter transition-colors"
654657
>
655-
<Settings size={14} />
656-
Settings
658+
<User size={14} />
659+
Profile
660+
</button>
661+
<button
662+
onClick={() => {
663+
setShowUserMenu(false)
664+
setActiveView('settings')
665+
setTimeout(() => {
666+
window.dispatchEvent(new CustomEvent('navigate-settings-tab', { detail: 'preferences' }))
667+
}, 0)
668+
}}
669+
className="w-full flex items-center gap-3 px-4 py-2 text-sm text-plm-fg hover:bg-plm-bg-lighter transition-colors"
670+
>
671+
<SlidersHorizontal size={14} />
672+
Preferences
657673
</button>
658674
</div>
659675

src/components/SettingsContent.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
2-
AccountSettings,
2+
ProfileSettings,
3+
PreferencesSettings,
34
OrganizationSettings,
45
CompanyProfileSettings,
56
RFQSettings,
@@ -12,10 +13,11 @@ import {
1213
WebhooksSettings,
1314
ApiSettings,
1415
LogsSettings,
15-
AboutSettings
16+
AboutSettings,
17+
SupabaseSettings
1618
} from './settings'
1719

18-
type SettingsTab = 'account' | 'organization' | 'company-profile' | 'rfq' | 'metadata-columns' | 'backup' | 'solidworks' | 'google-drive' | 'odoo' | 'slack' | 'webhooks' | 'api' | 'logs' | 'about'
20+
type SettingsTab = 'profile' | 'preferences' | 'organization' | 'company-profile' | 'rfq' | 'metadata-columns' | 'backup' | 'solidworks' | 'google-drive' | 'odoo' | 'slack' | 'webhooks' | 'api' | 'supabase' | 'logs' | 'about'
1921

2022
interface SettingsContentProps {
2123
activeTab: SettingsTab
@@ -24,8 +26,10 @@ interface SettingsContentProps {
2426
export function SettingsContent({ activeTab }: SettingsContentProps) {
2527
const renderContent = () => {
2628
switch (activeTab) {
27-
case 'account':
28-
return <AccountSettings />
29+
case 'profile':
30+
return <ProfileSettings />
31+
case 'preferences':
32+
return <PreferencesSettings />
2933
case 'organization':
3034
return <OrganizationSettings />
3135
case 'company-profile':
@@ -48,12 +52,14 @@ export function SettingsContent({ activeTab }: SettingsContentProps) {
4852
return <WebhooksSettings />
4953
case 'api':
5054
return <ApiSettings />
55+
case 'supabase':
56+
return <SupabaseSettings />
5157
case 'logs':
5258
return <LogsSettings />
5359
case 'about':
5460
return <AboutSettings />
5561
default:
56-
return <AccountSettings />
62+
return <ProfileSettings />
5763
}
5864
}
5965

src/components/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { GoogleDriveView } from './sidebar/GoogleDriveView'
2121
// System Views
2222
import { SettingsNavigation } from './sidebar/SettingsNavigation'
2323

24-
type SettingsTab = 'account' | 'vault' | 'organization' | 'company-profile' | 'rfq' | 'metadata-columns' | 'backup' | 'solidworks' | 'google-drive' | 'odoo' | 'slack' | 'webhooks' | 'api' | 'preferences' | 'logs' | 'about'
24+
type SettingsTab = 'profile' | 'preferences' | 'vault' | 'organization' | 'company-profile' | 'rfq' | 'metadata-columns' | 'backup' | 'solidworks' | 'google-drive' | 'odoo' | 'slack' | 'webhooks' | 'api' | 'logs' | 'about'
2525

2626
interface SidebarProps {
2727
onOpenVault: () => void
@@ -34,7 +34,7 @@ interface SidebarProps {
3434
// Fixed width for settings view (not resizable)
3535
const SETTINGS_SIDEBAR_WIDTH = 200
3636

37-
export function Sidebar({ onOpenVault, onOpenRecentVault, onRefresh, settingsTab = 'account', onSettingsTabChange }: SidebarProps) {
37+
export function Sidebar({ onOpenVault, onOpenRecentVault, onRefresh, settingsTab = 'profile', onSettingsTabChange }: SidebarProps) {
3838
const { activeView, sidebarWidth, connectedVaults } = usePDMStore()
3939

4040
// Settings view uses fixed width, others use resizable width

src/components/settings/AccountSettings.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { signOut, getSupabaseClient, endRemoteSession } from '../../lib/supabase
2626
import { getInitials } from '../../types/pdm'
2727
import { getMachineId } from '../../lib/backup'
2828
import { useTranslation } from '../../lib/i18n'
29+
import { ContributionHistory } from './ContributionHistory'
2930

3031
interface UserSession {
3132
id: string
@@ -265,6 +266,9 @@ export function AccountSettings() {
265266
</div>
266267
</section>
267268

269+
{/* Contribution History */}
270+
<ContributionHistory />
271+
268272
{/* Sessions */}
269273
<section>
270274
<h2 className="text-sm text-plm-fg-muted uppercase tracking-wide font-medium mb-3">

0 commit comments

Comments
 (0)