Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
159 changes: 122 additions & 37 deletions CHANGELOG.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Stores use Svelte 5 runes (`.svelte.js` / `.svelte.ts` files):

- `nodes.svelte.ts` - Central store for flow nodes/edges and the registry. Exports `getNodes()`, `getNodesSnapshot()`, `setNodes()`, `getEdges()`, `getEdgesSnapshot()`, `setEdges()`, `setRegistry()`. Network nodes use dedicated `RegisteredNetworkNodes` / `NetworkNodeOfTypeNetwork` types.
- `auth.svelte.js` - JWT token for coral-remote-server API
- `settingsStore.svelte.js` - User settings (SSH key path, visualizer URL)
- `settingsStore.svelte.js` - User settings stored under a single `'settings'` key in electron-store. Exports named key constants (`SSH_PATH`, `URL_VISUALIZER`, `URL_REMOTE_SERVER`, `USE_MPI`) and a `settingsState` object with `getKey(key)` / `setKey(key, value)` methods.
- `currentProjectStore.svelte.js` - Current project metadata
- `jobsStore.svelte.js` - Slurm job tracking

Expand Down Expand Up @@ -225,6 +225,9 @@ cd /app && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
- **IPC channels**: `execute-ssh-with-key`, `export-graph-ssh`, `set-theme`, `open-external-url`
- **Pre-commit hooks**: Husky runs `npm run lint` then Prettier; lint failures abort commit
- **API requests**: All authenticated requests go through `src/lib/requests/api.js` which auto-attaches the Bearer token
- **Slurm batch templates**: Two templates in `src/lib/templates/` — `sbatch.template.sh` (non-MPI) and `sbatch-mpi.template.sh` (MPI via `mpirun --allow-run-as-root -np ${SLURM_NTASKS:-1}`). Imported at build time via Vite's `?raw` suffix. `sshMessages.ts` selects between them based on the `USE_MPI` setting. Both templates expose `{{INTERNAL_JOB_ID}}` (used for `--touch-dir nodes-exec-status/<id>`) and `{{TIME_LIMIT}}`. The MPI template additionally exposes `{{NODES}}` and `{{NTASKS_PER_NODE}}`, filled at runtime from `JobConfig` (defaults: 1 node, 4 tasks/node, 01:00:00 time limit). Clicking Execute always opens `JobConfigModal.svelte` (renamed from `MpiConfigModal.svelte`) — it shows MPI-specific fields (nodes, tasks/node) only when MPI is enabled, and always shows the time limit field.
- **MPI graph payload**: When MPI is enabled, `buildGraphPayload()` in `sshMessages.ts` injects a `plugin: { MPI: { enabled: true, max_num_threads: 1 } }` block at the top of the exported network JSON, as required by CORAL for MPI initialization.
- **Node execution status**: `getNodesExecutionStatus(jobIdInternal)` reads files from `/app/shared-data/nodes-exec-status/<internalJobId>/` on the remote server, returning a `Map<qualifiedNodeId, string[]>` of status sequences (e.g. `'running'`, `'succeeded'`, `'failed'`).

## Git Workflow

Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,21 @@ Run unit tests

For more options see the [general instructions](https://www.electronjs.org/docs/latest/tutorial/debugging-main-process) or the [specific ones](https://www.electronjs.org/docs/latest/tutorial/debugging-vscode) for VS Code

### Debugging Svelte
#### Inspecting the Electron Store

The app uses [electron-store](https://github.com/sindresorhus/electron-store) to persist data. You can inspect and modify it from the DevTools console (**CTRL+SHIFT+I**):

```js
// Get a value
await window.electron.store.get('jobIdMap')

// Remove a value
await window.electron.store.remove('jobIdMap')
```

Available keys are defined in [electron/utils/storage.js](electron/utils/storage.js).

## Debugging Svelte

- Execute `npm run dev` and open the Source tab in the Chormium dev tools (**CTRL+SHIFT+I**). Then manually add the folder containing this repository from the Workspace sub-tab. Now add your breakpoints and start debugging.
- In Svelte code you can also use [`{@debug}`](https://svelte.dev/docs/svelte/@debug) or [`$inspect`](https://svelte.dev/docs/svelte/$inspect).
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ services:
- '8000:8000'
volumes:
- ./containers/shared-data:/app/shared-data
environment:
- OMPI_ALLOW_RUN_AS_ROOT=1
- OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
networks:
- coral-network
# restart: unless-stopped
Expand Down
9 changes: 3 additions & 6 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path'
import { fileURLToPath } from 'url'

import {
connectAndUploadGraph,
uploadFileViaSftp,
connectToSSHWithKey,
connectToSSHWithPassword,
} from './utils/sshConnections.js'
Expand Down Expand Up @@ -65,16 +65,13 @@ ipcMain.handle('execute-ssh-with-key', async (event, { command }) => {
return await connectToSSHWithKey(command, pathToSsh)
})

ipcMain.handle('export-graph-ssh', async (event, { graph }) => {
ipcMain.handle('upload-file-ssh', async (event, { content, remotePath }) => {
const settings = store.get('settings', {})
const pathToSsh = settings.sshPathKey
if (!pathToSsh) {
throw new Error('SSH key path not configured in settings')
}
const jsonGraph = JSON.stringify(graph)
console.log('exported graph', jsonGraph)
const remotePath = '/app/shared-data/graph.json'
return await connectAndUploadGraph(jsonGraph, remotePath, pathToSsh)
return await uploadFileViaSftp(content, remotePath, pathToSsh)
})

ipcMain.handle('open-external-url', async (event, url) => {
Expand Down
10 changes: 5 additions & 5 deletions electron/utils/sshConnections.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,23 @@ function connectToSSHWithKey(command, pathToSsh) {
})
}

function connectAndUploadGraph(jsonGraph, remotePath, pathToSsh) {
function uploadFileViaSftp(content, remotePath, pathToSsh) {
return new Promise((resolve, reject) => {
console.log('connectAndUploadGraph called')
console.log('uploadFileViaSftp called')
const conn = new Client()
conn
.on('ready', () => {
console.log('SSH Connection with key established')
conn.sftp((err, sftp) => {
if (err) return reject(err)
sftp.writeFile(remotePath, jsonGraph, (err) => {
sftp.writeFile(remotePath, content, (err) => {
if (err) {
conn.end()
return reject(err)
}
console.log('File uploaded successfully')
conn.end()
resolve(`Graph uploaded to: ${remotePath}`)
resolve(`File uploaded to: ${remotePath}`)
})
})
})
Expand All @@ -120,4 +120,4 @@ function connectAndUploadGraph(jsonGraph, remotePath, pathToSsh) {
})
}

export { connectToSSHWithPassword, connectToSSHWithKey, connectAndUploadGraph }
export { connectToSSHWithPassword, connectToSSHWithKey, uploadFileViaSftp }
7 changes: 6 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export default defineConfig([
},
{
plugins: {},
rules: {},
rules: {
// Allow underscore-prefixed names to be declared but unused.
// This is needed for callback parameter names in TypeScript interface definitions,
// See: https://eslint.org/docs/latest/rules/no-unused-vars#argsignorepattern
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
])
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
},
"scripts": {
"dev:vite": "vite",
"dev": "npm run build:dev && cross-env NODE_ENV=development electron .",
"dev": "npm run build:dev && electron --no-sandbox .",
"build:dev": "vite build --config vite.config.dev.js",
"build": "vite build",
"preview": "vite preview",
"start": "electron .",
"start": "electron --no-sandbox .",
"start:debug": "electron . --inspect=9229",
"start:forge": "electron-forge start",
"package": "electron-forge package",
Expand Down
186 changes: 186 additions & 0 deletions src/lib/components/JobConfigModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<script lang="ts">
import Modal, { getModal } from './layout/Modal.svelte'
import Button from './layout/Button.svelte'
import type { JobConfig } from '../utils/sshMessages'

interface Props {
modalId: string
showMpiFields: boolean
// underscore-prefixed arg name to avoid eslint no-unused-vars error in interfaces
onConfirm: (_config: JobConfig) => void
}

let { modalId, showMpiFields, onConfirm }: Props = $props()

let nodes = $state(1)
let tasksPerNode = $state(4)
let timeLimit = $state('01:00:00')
let totalProcesses = $derived(nodes * tasksPerNode)

// Slurm --time accepted formats: minutes | minutes:seconds | hours:minutes:seconds
// | days-hours | days-hours:minutes | days-hours:minutes:seconds | 0
// See: https://slurm.schedmd.com/sbatch.html#OPT_time
const SLURM_TIME_PATTERN =
/^(\d+|\d+:\d{2}(:\d{2})?|\d+-\d{2}(:\d{2}(:\d{2})?)?)$/
let timeLimitError = $derived(
SLURM_TIME_PATTERN.test(timeLimit)
? ''
: 'Use: minutes, minutes:seconds, HH:MM:SS, D-HH, D-HH:MM, D-HH:MM:SS'
)

const handleConfirm = () => {
onConfirm({ nodes, tasksPerNode, timeLimit })
getModal(modalId).close()
}

const handleCancel = () => {
getModal(modalId).close()
}
</script>

<Modal id={modalId} size="sm">
<div class="job-config">
<h2>Job Configuration</h2>
<div class="inputs-container">
{#if showMpiFields}
<div class="inputs-row">
<div class="input-container">
<label for="mpi-nodes">Nodes</label>
<input
id="mpi-nodes"
type="number"
min="1"
bind:value={nodes}
class="input-field"
/>
</div>
<div class="input-container">
<label for="mpi-tasks-per-node">Tasks per node</label>
<input
id="mpi-tasks-per-node"
type="number"
min="1"
bind:value={tasksPerNode}
class="input-field"
/>
</div>
<div class="input-container total">
<span class="total-label">Total</span>
<span class="total-value">{totalProcesses}</span>
</div>
</div>
{/if}
<div class="inputs-row">
<div class="input-container time-limit">
<label for="job-time-limit">Time limit</label>
<input
id="job-time-limit"
type="text"
placeholder="e.g. 01:00:00"
bind:value={timeLimit}
class="input-field"
class:input-field--error={timeLimitError}
/>
<span class="hint-message" class:hint-message--error={timeLimitError}>
{timeLimitError || 'Use 0 for no time limit'}
</span>
</div>
</div>
</div>
<div class="button-container">
<Button size="small" onclick={handleCancel}>Cancel</Button>
<Button
variant="action"
size="small"
onclick={handleConfirm}
disabled={!!timeLimitError}
>
Execute
</Button>
</div>
</div>
</Modal>

<style>
.job-config {
padding: 1rem;
}

.job-config h2 {
margin: 0 0 1.5rem 0;
text-align: center;
}

.inputs-container {
display: flex;
flex-direction: column;
gap: 1vh;
padding: 2vh 0;
}

.inputs-row {
display: flex;
flex-direction: row;
gap: 2vh;
}

.input-container {
display: flex;
flex-direction: column;
gap: 1vh;
flex: 1;
min-width: 0;
}

.input-container label,
.total-label {
font-weight: bold;
}

.input-field {
padding: 1vh;
border: 1px solid var(--ternary-color);
border-radius: 8px;
font-size: 1rem;
background: var(--secondary-color);
}

.input-field--error {
border-color: var(--error-color, #e53935);
}

.hint-message {
font-size: 0.8rem;
color: var(--ternary-color);
}

.hint-message--error {
color: var(--error-color, #e53935);
}

.total {
border-left: 1px solid var(--ternary-color);
padding-left: 2vh;
flex: 0 0 auto;
}

.total-value {
font-size: 1.1rem;
font-weight: bold;
color: var(--button-action-bg);
padding: 1vh 0;
}

.time-limit {
flex: 0 1 auto;
min-width: 10rem;
max-width: 16rem;
}

.button-container {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
</style>
6 changes: 6 additions & 0 deletions src/lib/components/LoginForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@
</div>

<style>
h2 {
margin: 0 0 1.5rem 0;
text-align: center;
}

.inputs-container {
display: flex;
flex-direction: row;
gap: 2vh;
padding: 1vh 0;
}

.input-container {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/ProjectCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@

interface Props {
project: Project
// eslint-disable-next-line no-unused-vars
onDelete: (projectId: number) => void
// underscore-prefixed arg name to avoid eslint no-unused-vars error in interfaces
onDelete: (_projectId: number) => void
onLoad: () => void
onShare: () => void
onEdit: () => void
Expand Down
5 changes: 5 additions & 0 deletions src/lib/components/SaveProjectForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
</div>

<style>
h2 {
margin: 0 0 1.5rem 0;
text-align: center;
}

.inputs-container {
display: flex;
flex-direction: column;
Expand Down
Loading