Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ export default tseslint.config(
'@typescript-eslint/unbound-method': 'warn',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
// `crypto.randomUUID` is secure-context-only. autonomy-node serves the web
// bundle over plain HTTP, so on a node reached by IP the global is absent and
// the call throws — which is how a build in the web editor once aborted in
// silence. openplc-editor's own renderer always has a secure context, but
// `frontend/` and `middleware/shared/` are byte-identical across the two
// repos, so a call added in either one ships that silent failure in the web
// bundle. One guarded call site did not stop the other 62 from shipping
// unguarded; the rule is what holds the line. Every mint goes through
// `newUuid()`, which names this API nowhere, so the rule needs no exception.
'no-restricted-properties': [
'error',
{
object: 'crypto',
property: 'randomUUID',
message:
'crypto.randomUUID does not exist outside a secure context (autonomy-node serves over plain HTTP). Use newUuid() from frontend/utils/new-uuid.ts, or let the owning store mint the id.',
},
],
},
},
eslintConfigPrettier,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Node } from '@xyflow/react'
import { ComponentPropsWithRef, forwardRef, useEffect, useMemo, useState } from 'react'

Expand All @@ -12,6 +12,7 @@
import type { CreateGraphicalVariableModalData } from '../../../../../store/slices/modal/types'
import { cn } from '../../../../../utils/cn'
import { getLiteralType, isLegalIdentifier } from '../../../../../utils/keywords'
import { newUuid } from '../../../../../utils/new-uuid'
import type { BoundBlockPin } from '../../../../../utils/PLC/validate-variable-type'
import { isGenericTypeName } from '../../../../../utils/PLC/validate-variable-type'
import { toast } from '../../../../_features/[app]/toast/use-toast'
Expand Down Expand Up @@ -260,7 +261,7 @@

const res = createVariable({
data: {
id: crypto.randomUUID(),
id: newUuid(),
name,
type: {
definition: type.definition,
Expand Down Expand Up @@ -296,7 +297,7 @@

const submitCreateANewBlock = (blockType: CustomFbdNodeTypes) => {
const newBlock = buildGenericNode({
id: crypto.randomUUID(),
id: newUuid(),
position:
block.positionAbsoluteX && block.positionAbsoluteY
? { x: block.positionAbsoluteX, y: block.positionAbsoluteY + (block.height ?? 0) + 16 }
Expand Down
9 changes: 4 additions & 5 deletions src/frontend/components/_atoms/graphical-editor/fbd/block.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FocusEvent, memo, useEffect, useMemo, useRef, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'

import type { PLCVariable } from '../../../../../middleware/shared/ports/types'
import { RefreshIcon } from '../../../../assets/icons/interface/Refresh'
Expand All @@ -12,6 +11,7 @@
rewireInOutReads,
} from '../../../../utils/graphical/in-out-pin-rules'
import { isLegalIdentifier } from '../../../../utils/keywords'
import { newUuid } from '../../../../utils/new-uuid'
import { toast } from '../../../_features/[app]/toast/use-toast'
import { useBoundEditorModel, useBoundPou } from '../../../_features/[workspace]/editor/graphical/active-context'
import { HighlightedTextArea } from '../../highlighted-textarea'
Expand Down Expand Up @@ -228,7 +228,7 @@
* The new block node have a new ID to not conflict with the old block node and to no occur any error of rendering
*/
const newBlockNode = buildBlockNode({
id: `BLOCK_${crypto.randomUUID()}`,
id: `BLOCK_${newUuid()}`,
position: {
x: node.position.x,
y: node.position.y,
Expand Down Expand Up @@ -554,7 +554,7 @@

const creationResult = createVariable({
data: {
id: uuidv4(),
id: newUuid(),
name: variableNameToSubmit,
type: { definition: 'derived', value: blockType },
class: 'local',
Expand Down Expand Up @@ -660,7 +660,7 @@
}

const updatedNewNode = buildBlockNode({
id: `BLOCK_${crypto.randomUUID()}`,
id: `BLOCK_${newUuid()}`,
position: {
x: node.position.x,
y: node.position.y,
Expand Down Expand Up @@ -792,7 +792,6 @@
// has to be told rather than left to notice a missing connection later.
if (reads.rewired > 0 || reads.dropped > 0) {
addLog({
id: crypto.randomUUID(),
level: reads.dropped > 0 ? 'warning' : 'info',
message:
`${blockVariantName}: VAR_IN_OUT pins no longer have an output side. ` +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Pencil1Icon, TrashIcon } from '@radix-ui/react-icons'
import type { ModbusIOGroup, ModbusIOPoint } from '@root/middleware/shared/ports/types'
import { useRuntime } from '@root/middleware/shared/providers/platform-context'
Expand Down Expand Up @@ -600,15 +600,13 @@
setSerialPortOptions(options)
} else {
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'warning',
message: `Failed to fetch serial ports: ${result.error || 'Unknown error'}`,
})
setSerialPortOptions([])
}
} catch (error) {
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'warning',
message: `Error fetching serial ports: ${getErrorMessage(error)}`,
})
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/_organisms/plc-logs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { debounce } from 'lodash'
import { memo, useEffect, useMemo, useRef } from 'react'

import { isV4Logs, RuntimeLogEntry, RuntimeLogLevel } from '../../../../middleware/shared/ports'
import { useOpenPLCStore } from '../../../store'
import formatTimestamp from '../../../utils/format-timestamp'
import { newUuid } from '../../../utils/new-uuid'
import { LogComponent, LogLevel } from '../console/log'

const mapV4LevelToLogLevel = (level: RuntimeLogLevel): LogLevel => {
Expand Down Expand Up @@ -70,7 +71,7 @@
// Generate stable UUIDs for v3 log lines
const getV3LogKey = (index: number): string => {
if (!v3LogKeysRef.current.has(index)) {
v3LogKeysRef.current.set(index, crypto.randomUUID())
v3LogKeysRef.current.set(index, newUuid())
}
return v3LogKeysRef.current.get(index)!
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { evaluatePreBuildPlcGate } from '@root/middleware/shared/utils/build-gate/pre-build-plc-gate'
import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities'
import { useCallback, useEffect, useRef, useState } from 'react'
Expand Down Expand Up @@ -160,7 +160,6 @@
if (!useOpenPLCStore.getState().workspace.isDebuggerVisible) return

addLog({
id: crypto.randomUUID(),
level: 'warning',
message: 'Device disconnected — stopping the debug session (serial debugging runs over the device connection).',
})
Expand All @@ -176,7 +175,6 @@

if (wasSimulator && !isSimulatorBoard && simulator.isRunning()) {
addLog({
id: crypto.randomUUID(),
level: 'info',
message: 'Board changed from simulator. Stopping simulator.',
})
Expand Down Expand Up @@ -278,19 +276,18 @@
}
if (!stopResult.success) {
addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Failed to stop PLC: ${stopResult.error ?? 'Unknown error'}`,
})
setIsCompiling(false)
return
}
useOpenPLCStore.getState().deviceActions.setPlcRuntimeStatus('STOPPED')
addLog({ id: crypto.randomUUID(), level: 'info', message: 'PLC stopped before build.' })
addLog({ level: 'info', message: 'PLC stopped before build.' })
}
}

addLog({ id: crypto.randomUUID(), level: 'info', message: 'Build process started' })
addLog({ level: 'info', message: 'Build process started' })

// Compile-time alias resolution: snapshot the project with every
// variable's `location` resolved to a concrete IEC address (alias name
Expand Down Expand Up @@ -364,7 +361,7 @@
void simulator.loadFirmware(event.firmwarePath).then((loadResult) => {
if (loadResult.success) {
setSimulatorRunning(true)
addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator is running.' })
addLog({ level: 'info', message: 'Simulator is running.' })
if (pendingSimulatorDebugRef.current) {
pendingSimulatorDebugRef.current = false
// Rides the emulator's session, so it ends when the emulator
Expand All @@ -377,7 +374,6 @@
} else {
pendingSimulatorDebugRef.current = false
addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Failed to start simulator: ${loadResult.error ?? 'Unknown error'}`,
})
Expand All @@ -388,7 +384,7 @@
)

if (!result.success && !streamedError) {
addLog({ id: crypto.randomUUID(), level: 'error', message: result.error ?? 'Compilation failed' })
addLog({ level: 'error', message: result.error ?? 'Compilation failed' })
}

// Serial handoff (D72): if we released a held device connection for this
Expand Down Expand Up @@ -417,7 +413,7 @@
}
}
} catch (err: unknown) {
addLog({ id: crypto.randomUUID(), level: 'error', message: `Build error: ${getErrorMessage(err)}` })
addLog({ level: 'error', message: `Build error: ${getErrorMessage(err)}` })
} finally {
setIsCompiling(false)
}
Expand Down Expand Up @@ -478,7 +474,6 @@

if (!compiler.compileLibrary) {
addLog({
id: crypto.randomUUID(),
level: 'error',
message: 'Current platform does not implement library builds.',
})
Expand All @@ -487,7 +482,6 @@

setIsCompiling(true)
addLog({
id: crypto.randomUUID(),
level: 'info',
message: overrides?.cleanBuild ? 'Library build started (clean)' : 'Library build started',
})
Expand All @@ -498,28 +492,24 @@
(event) => {
if (!event.message) return
addLog({
id: crypto.randomUUID(),
level: event.level === 'error' || event.stage === 'error' ? 'error' : 'info',
message: event.message,
})
},
)
if (!result.success) {
addLog({
id: crypto.randomUUID(),
level: 'error',
message: result.error ?? 'Library build failed.',
})
} else if (result.verification && !result.verification.success) {
addLog({
id: crypto.randomUUID(),
level: 'warning',
message: `Library built, but verification reported: ${result.verification.message ?? 'unknown'}`,
})
}
} catch (err) {
addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Library build error: ${getErrorMessage(err)}`,
})
Expand Down Expand Up @@ -594,7 +584,6 @@

if (result.unsupported) {
addLog({
id: crypto.randomUUID(),
level: 'info',
message: 'This firmware predates run/stop control. Rebuild and upload the program to enable Start/Stop.',
})
Expand All @@ -608,7 +597,6 @@
}
if (!result.success) {
addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Failed to ${wantRun ? 'start' : 'stop'} PLC: ${result.error ?? 'Unknown error'}`,
})
Expand All @@ -629,7 +617,7 @@
)
}
} catch (error: unknown) {
addLog({ id: crypto.randomUUID(), level: 'error', message: `PLC control error: ${getErrorMessage(error)}` })
addLog({ level: 'error', message: `PLC control error: ${getErrorMessage(error)}` })
}
}, [
deviceDefinitions.configuration.deviceBoard,
Expand Down Expand Up @@ -681,7 +669,7 @@
debugSessionRidesDeviceRef.current = false

setSimulatorRunning(false)
addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator stopped.' })
addLog({ level: 'info', message: 'Simulator stopped.' })
} else {
pendingSimulatorDebugRef.current = true
handleBuildRef.current().catch(() => {
Expand All @@ -690,7 +678,7 @@
}
} catch (error: unknown) {
pendingSimulatorDebugRef.current = false
addLog({ id: crypto.randomUUID(), level: 'error', message: `Simulator control error: ${getErrorMessage(error)}` })
addLog({ level: 'error', message: `Simulator control error: ${getErrorMessage(error)}` })
}
}, [debugSession, simulator, simulatorRunning, addLog])

Expand All @@ -711,12 +699,12 @@
['Yes', 'No'],
)
if (response === 1) {
consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', message: 'Debugger session cancelled.' })
consoleActions.addLog({ level: 'info', message: 'Debugger session cancelled.' })
setIsDebuggerProcessing(false)
return
}

consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', message: 'Starting PLC...' })
consoleActions.addLog({ level: 'info', message: 'Starting PLC...' })
const startResult = (await debuggerPort.setPlcState?.('RUNNING')) ?? {
success: false,
error: 'This target does not support run/stop control',
Expand All @@ -736,7 +724,7 @@
}

// Read local MD5
consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', message: 'Verifying program MD5...' })
consoleActions.addLog({ level: 'info', message: 'Verifying program MD5...' })
const md5Result = await debuggerPort.readProgramMd5(projectPath, boardTarget)
if (!md5Result.success || !md5Result.md5) {
await showDeviceDialog('error', 'MD5 Extraction Failed', md5Result.error ?? 'Could not extract MD5', ['OK'])
Expand Down Expand Up @@ -773,7 +761,7 @@
}

if (verifyResult.match) {
consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', message: 'MD5 verified. Starting debugger...' })
consoleActions.addLog({ level: 'info', message: 'MD5 verified. Starting debugger...' })
// Persist the target's byte order — detected from the MD5
// response trailer in the runtime — so the swap layer at the
// read / write boundaries flips on BE targets. Default to
Expand All @@ -787,7 +775,6 @@
await debuggerPort.disconnect()

consoleActions.addLog({
id: crypto.randomUUID(),
level: 'warning',
message: `MD5 mismatch. Target: ${verifyResult.targetMd5}, Expected: ${md5Result.md5}`,
})
Expand Down Expand Up @@ -816,15 +803,13 @@
)
if (compileResult.success) {
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'info',
message: 'Upload completed. Re-verifying...',
})
await new Promise((resolve) => setTimeout(resolve, 2000))
void handleMd5Verification(projectPath, boardTarget, isRuntimeTarget)
} else {
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Upload failed: ${compileResult.error ?? 'Unknown error'}`,
})
Expand All @@ -837,7 +822,6 @@
} catch (error: unknown) {
await debuggerPort.disconnect()
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'error',
message: `MD5 verification error: ${getErrorMessage(error)}`,
})
Expand Down Expand Up @@ -904,7 +888,6 @@
// login — all three publish the same status.
const sessionStatus = useOpenPLCStore.getState().deviceConnection.status
addLog({
id: crypto.randomUUID(),
level: 'info',
message: `[connection] debug session requested for ${boardTarget}; session is "${sessionStatus}"`,
})
Expand All @@ -928,14 +911,13 @@
// addresses first (same pre-compile snapshot the build/upload paths
// use) — the compiler only understands `%…` literals, not alias names.
const freshProjectData = useOpenPLCStore.getState().projectActions.getCompileReadyProjectData()
consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', message: 'Starting debug compilation...' })
consoleActions.addLog({ level: 'info', message: 'Starting debug compilation...' })
const debugCompileResult = await compiler.compileForDebug(
{ projectData: freshProjectData, boardTarget, projectPath },
(event) => logCompilerEvent(event, consoleActions.addLog),
)
if (!debugCompileResult.success) {
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Debug compilation failed: ${debugCompileResult.error ?? 'Unknown error'}`,
})
Expand All @@ -949,7 +931,6 @@
void handleMd5Verification(projectPath, boardTarget, isRuntime)
} catch (error: unknown) {
consoleActions.addLog({
id: crypto.randomUUID(),
level: 'error',
message: `Debugger init error: ${getErrorMessage(error)}`,
})
Expand Down
Loading
Loading