Skip to content

Commit 781a08e

Browse files
committed
Release v3.12.0
1 parent 8be6d20 commit 781a08e

File tree

76 files changed

+5470
-3197
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+5470
-3197
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

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

5-
## [3.11.1] - 2026-01-23
5+
## [3.12.0] - 2026-01-23
66

77
### Added
88
- **Type generation script**: New `npm run gen:types` command that loads `SUPABASE_ACCESS_TOKEN` from `.env` file and regenerates TypeScript types from the live database
9+
- **SolidWorks service versioning**: The SolidWorks service now reports its version, and the app checks for compatibility. Version mismatch warnings appear in the Service tab when the service is outdated or incompatible, with clear instructions to rebuild
10+
- **Metadata preservation when copying**: Copying files now preserves part number, description, and revision from the source file. Metadata is copied from pending local edits if present, otherwise from synced server data
11+
- **Checkout protection for destructive operations**: Files checked out by other users are now protected from delete, move, and rename operations. Commands show clear error messages indicating which files are locked and by whom. Context menu items appear disabled with "(locked)" indicator when selection includes files checked out by others. Drag-and-drop moves show "not allowed" cursor for locked files
12+
- **Document Manager-only mode**: The SolidWorks service can now run without a full SolidWorks installation. Users with just the Document Manager API license key can read/write file properties, extract BOMs, get configurations, read references, and extract previews. A new "Feature Availability" collapsible section in the Service tab shows which features work in each mode. Operations requiring full SolidWorks (exports, mass properties, Pack and Go, etc.) now return clear error messages with `SW_NOT_INSTALLED` error code instead of failing silently
913

1014
### Fixed
1115
- **Delete from server keeps file read-only**: Fixed issue where deleting a checked-in file from the server while keeping the local copy would leave the file read-only. Local-only files are now correctly made writable after the server deletion
@@ -16,6 +20,11 @@ All notable changes to BluePLM will be documented in this file.
1620

1721
### Changed
1822
- **Removed type workarounds**: Cleaned up `as any` type casts for `folders` table, `move_file` RPC, and `create_default_workflow` RPC now that types are regenerated
23+
- **Bulk delete performance overhaul**: Large file deletions no longer use optimistic UI updates. Files now remain visible with spinners during the deletion process, and both the file tree and main browser update together when the operation completes. This prevents visual inconsistencies where files would disappear then reappear if deletion failed
24+
- **Folder move reliability**: Moving folders now releases Document Manager file handles before the operation, cancels any queued thumbnail extractions, and checks for ongoing file operations (downloads, syncs) before proceeding. Previously, moves could fail with EPERM errors when files inside the folder were being processed
25+
- **Rename/move error handling**: Rename and move operations now detect locked files and identify the blocking process (e.g., "Cannot rename: file is in use by SLDWORKS.exe"). Operations retry up to 3 times with backoff before failing
26+
- **Folder copy accuracy**: Copying folders now accurately reports the total number of files copied (not just the folder count) and shows proper progress. Nested files inside copied folders are immediately visible in the UI without requiring a refresh
27+
- **Multi-machine folder sync**: When another user moves a folder, the app now batches all file location updates into a single render instead of processing each file individually. This prevents UI freezes when large folders are moved by teammates
1928

2029
### Removed
2130
- **Speculative parent assembly warning**: Removed the warning toast "Some files may have parent assemblies still checked out" that appeared when checking in parts or assemblies. This warning was overly aggressive and triggered false positives - it would warn even when the checked-out assemblies had nothing to do with the files being checked in

api/middleware/auth.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22
* BluePLM API Authentication Middleware
33
*
44
* Fastify plugin that validates JWT tokens and attaches user profile to requests.
5+
*
6+
* Security note: Verbose logging is disabled by default. Do not log tokens,
7+
* full user IDs, or email addresses in production.
58
*/
69

710
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'
811
import fp from 'fastify-plugin'
912
import { createSupabaseClient } from '../src/infrastructure/supabase.js'
1013
import type { UserProfile } from '../types.js'
1114

15+
/**
16+
* Truncate a UUID for safe logging (shows first 8 characters)
17+
*/
18+
function truncateId(id: string): string {
19+
return id.length > 8 ? `${id.substring(0, 8)}...` : id
20+
}
21+
1222
const authPluginImpl: FastifyPluginAsync = async (fastify) => {
1323
// Decorate request with user, supabase client, and access token
1424
fastify.decorateRequest('user', null)
@@ -20,14 +30,11 @@ const authPluginImpl: FastifyPluginAsync = async (fastify) => {
2030
request: FastifyRequest,
2131
reply: FastifyReply
2232
): Promise<void> {
23-
console.log('>>> [Auth] authenticate() ENTRY')
24-
2533
try {
2634
const authHeader = request.headers.authorization
27-
console.log('>>> [Auth] Header:', authHeader ? authHeader.substring(0, 30) + '...' : 'NONE')
2835

2936
if (!authHeader || !authHeader.startsWith('Bearer ')) {
30-
console.log('>>> [Auth] FAIL: Missing or invalid auth header')
37+
fastify.log.warn('[Auth] Missing or invalid auth header')
3138
reply.code(401).send({
3239
error: 'Unauthorized',
3340
message: 'Missing or invalid Authorization header'
@@ -38,20 +45,19 @@ const authPluginImpl: FastifyPluginAsync = async (fastify) => {
3845
const token = authHeader.substring(7)
3946

4047
if (!token || token === 'undefined' || token === 'null') {
41-
console.log('>>> [Auth] FAIL: Empty or invalid token string')
48+
fastify.log.warn('[Auth] Empty or invalid token string')
4249
reply.code(401).send({
4350
error: 'Unauthorized',
4451
message: 'Invalid or missing access token'
4552
})
4653
throw new Error('Auth: Invalid token string')
4754
}
4855

49-
console.log('>>> [Auth] Verifying token with Supabase...')
5056
const supabase = createSupabaseClient(token)
5157
const { data: { user }, error } = await supabase.auth.getUser(token)
5258

5359
if (error || !user) {
54-
console.log('>>> [Auth] FAIL: Token verification failed:', error?.message)
60+
fastify.log.warn('[Auth] Token verification failed')
5561
reply.code(401).send({
5662
error: 'Invalid token',
5763
message: error?.message || 'Token verification failed',
@@ -60,15 +66,14 @@ const authPluginImpl: FastifyPluginAsync = async (fastify) => {
6066
throw new Error('Auth: Token verification failed')
6167
}
6268

63-
console.log('>>> [Auth] Token valid, looking up profile for user:', user.id)
6469
const { data: profile, error: profileError } = await supabase
6570
.from('users')
6671
.select('id, email, role, org_id, full_name')
6772
.eq('id', user.id)
6873
.single()
6974

7075
if (profileError || !profile) {
71-
console.log('>>> [Auth] FAIL: Profile lookup failed:', profileError?.message)
76+
fastify.log.warn({ msg: '[Auth] Profile lookup failed', userId: truncateId(user.id) })
7277
reply.code(401).send({
7378
error: 'Profile not found',
7479
message: 'User profile does not exist'
@@ -77,7 +82,7 @@ const authPluginImpl: FastifyPluginAsync = async (fastify) => {
7782
}
7883

7984
if (!profile.org_id) {
80-
console.log('>>> [Auth] FAIL: User has no organization:', profile.email)
85+
fastify.log.warn({ msg: '[Auth] User has no organization', userId: truncateId(user.id) })
8186
reply.code(403).send({
8287
error: 'No organization',
8388
message: 'User is not a member of any organization'
@@ -89,11 +94,11 @@ const authPluginImpl: FastifyPluginAsync = async (fastify) => {
8994
request.user = profile as UserProfile
9095
request.supabase = supabase
9196
request.accessToken = token
92-
console.log('>>> [Auth] SUCCESS: Authenticated', profile.email)
93-
fastify.log.info({ msg: '>>> [Auth] Authenticated', email: profile.email })
97+
98+
// Log success with minimal info (no email, truncated ID)
99+
fastify.log.debug({ msg: '[Auth] Authenticated', userId: truncateId(profile.id) })
94100
} catch (err) {
95-
// Re-throw to stop the request lifecycle
96-
console.log('>>> [Auth] Exception caught:', err instanceof Error ? err.message : err)
101+
// Re-throw to stop the request lifecycle (error already logged above)
97102
throw err
98103
}
99104
})

electron/handlers/extensionHost.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,6 @@ function getExtensionsPath(): string {
113113
function loadExtensionsFromDisk(): void {
114114
const extensionsPath = getExtensionsPath()
115115

116-
// #region agent log
117-
fetch('http://127.0.0.1:7242/ingest/54b4ff62-a662-4a7e-94d3-5e04211d678b',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'extensionHost.ts:loadExtensionsFromDisk',message:'Scanning extensions directory',data:{extensionsPath},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H1'})}).catch(()=>{});
118-
// #endregion
119116

120117
try {
121118
// Get all directories in the extensions folder
@@ -177,9 +174,6 @@ function loadExtensionsFromDisk(): void {
177174
installedAt,
178175
})
179176

180-
// #region agent log
181-
fetch('http://127.0.0.1:7242/ingest/54b4ff62-a662-4a7e-94d3-5e04211d678b',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'extensionHost.ts:loadExtensionsFromDisk:registered',message:'Registered extension from disk',data:{dirName:dir.name,manifestId:manifest.id,version:manifest.version,name:manifest.name},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H1'})}).catch(()=>{});
182-
// #endregion
183177

184178
deps?.log(`Loaded extension from disk: ${manifest.id} v${manifest.version}`)
185179

@@ -801,9 +795,6 @@ export function registerExtensionHostHandlers(
801795
throw new Error('Store API returned unsuccessful response')
802796
}
803797

804-
// #region agent log
805-
fetch('http://127.0.0.1:7242/ingest/54b4ff62-a662-4a7e-94d3-5e04211d678b',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'extensionHost.ts:fetch-store:raw',message:'Raw store API response',data:{count:result.data.length,firstExt:result.data[0]},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H2'})}).catch(()=>{});
806-
// #endregion
807798

808799
// Transform to StoreExtensionInfo format
809800
const extensions = result.data.map(ext => ({
@@ -1005,9 +996,6 @@ export function registerExtensionHostHandlers(
1005996
// downloadId: database UUID for download URL
1006997
// manifestId: optional expected manifest ID (publisher.slug + name) for validation
1007998
ipcMain.handle('extensions:install', async (_event, downloadId: string, version?: string, manifestId?: string) => {
1008-
// #region agent log
1009-
fetch('http://127.0.0.1:7242/ingest/54b4ff62-a662-4a7e-94d3-5e04211d678b',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'extensionHost.ts:install:entry',message:'Install request received',data:{downloadId,manifestId,version},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H3'})}).catch(()=>{});
1010-
// #endregion
1011999
deps?.log(`Installing extension: ${downloadId}${version ? `@${version}` : ''}`)
10121000

10131001
try {
@@ -1016,9 +1004,6 @@ export function registerExtensionHostHandlers(
10161004
? `${STORE_API_URL}/store/extensions/${encodeURIComponent(downloadId)}/download/${encodeURIComponent(version)}`
10171005
: `${STORE_API_URL}/store/extensions/${encodeURIComponent(downloadId)}/download`
10181006

1019-
// #region agent log
1020-
fetch('http://127.0.0.1:7242/ingest/54b4ff62-a662-4a7e-94d3-5e04211d678b',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'extensionHost.ts:install:downloadUrl',message:'Download URL constructed',data:{downloadUrl,downloadId,manifestId},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H2'})}).catch(()=>{});
1021-
// #endregion
10221007
deps?.log(`Downloading from: ${downloadUrl}`)
10231008

10241009
const response = await fetch(downloadUrl, { redirect: 'follow' })

0 commit comments

Comments
 (0)